Skip to content

Improvement for TEC correction - #369

Open
seongsujeong wants to merge 12 commits into
isce-framework:developfrom
seongsujeong:tec_correction_config
Open

seongsujeong wants to merge 12 commits into
isce-framework:developfrom
seongsujeong:tec_correction_config

Conversation

@seongsujeong

@seongsujeong seongsujeong commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

This PR adds several options to ionospheric TEC correction

  • Add option into runconfig to use total TEC only
  • Add option to turn on / off the slant range / azimuth correction individually
  • Add logic to ignore topside TEC (i.e. use total TEC only) based on the data flag inside IMAGEN TEC
  • Apply polynomial fitting to TEC profile to get rid of spikes
  • Add the additional TEC profiles to GCOV and InSAR workflows

EDIT
09/10/2026: RANSAC-based outlier detection is implemented and being applied before polynomial fitting. Test is ongoing internally.

@hfattahi hfattahi added this to the R05.03.0 milestone Aug 25, 2026
Comment thread python/packages/nisar/workflows/geocode_corrections.py Outdated
Comment thread python/packages/nisar/workflows/geocode_corrections.py Outdated
Comment thread python/packages/nisar/workflows/geocode_corrections.py Outdated

@hfattahi hfattahi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some comments up to what we reviewed today.

Comment thread share/nisar/schemas/gslc.yaml Outdated
Comment thread share/nisar/schemas/gcov.yaml Outdated
Comment thread share/nisar/schemas/insar.yaml Outdated
Comment thread share/nisar/schemas/gcov.yaml
Comment thread share/nisar/defaults/gcov.yaml Outdated
# NOTE: Just returning an empty LUT2d() will disrupt the downstream procedure,
# especially comparing the LUT's axis with the RSLC's radargrid
zero_arr = np.zeros(tec_correction.data.shape)
tec_correction = isce3.core.LUT2d(tec_correction.x_axis,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

Comment thread python/packages/nisar/workflows/geocode_corrections.py Outdated
Comment thread python/packages/nisar/workflows/geocode_corrections.py Outdated
Comment thread python/packages/nisar/workflows/geocode_corrections.py Outdated
Comment thread python/packages/isce3/atmosphere/tec_product.py Outdated

@bhawkins bhawkins left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polynomial fitting might not be robust to outliers, so another approach like median filter or RANSAC algorithm might be better.

@gshiroma suggests checking that the related output layers (az and rg corrections, but not TEC, which get geocoded) in the GCOV product still make sense in all cases.

Comment thread python/packages/isce3/atmosphere/tec_product.py
Comment thread python/packages/isce3/atmosphere/tec_product.py
@seongsujeong

Copy link
Copy Markdown
Contributor Author

@gshiroma suggests checking that the related output layers (az and rg corrections, but not TEC, which get geocoded) in the GCOV product still make sense in all cases.

In GCOV, disabling either correction (i.e., the azimuth or slant-range correction) causes the corresponding correction LUT to be omitted from the output. @gshiroma Please confirm if this is expected behavior.

@rad-eng-59 rad-eng-59 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the updates. Just some minor comments.

Comment thread python/packages/isce3/atmosphere/tec_product.py Outdated
return delta_r


def _ransac_polyfit(x: np.ndarray, y: np.ndarray, degree: int,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this is overkill. You could have just use MAD with either unity scalar (strict filtering) or factor of 2 scalar (looser filtering) to remove outliers from your data population prior to polyfitting for a certain degree.
It seems like it is assumed that the noise in measured TEC values has normal distribution by default and thus outliers are treated as tails beyond 3xSigma. Not sure whether this is correct assumption about TEC measurements and their source of error given small number of samples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After testing and tuning the algorithm, it look like MAD-based filtering gives better results. I changed the implementation to MAD-based.

Comment thread python/packages/isce3/atmosphere/tec_product.py Outdated
sigma: float=1.5) -> np.ndarray:
'''
Robustly fit a polynomial to a TEC profile by rejecting noisy samples using
a MAD-based threshold.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, introduced the acronym MAD (Median Absolute Deviation).

coeffs = np.polyfit(x, y, degree)
resid = y - np.polyval(coeffs, x)

# Robust noise scale: k * 1.4826 * MAD of the residuals.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this "k * 1.4826"?

# Keep only inliers, but fall back to the plain fit if too few survive.
inliers = np.abs(resid) <= threshold
if np.count_nonzero(inliers) <= degree:
info_channel.log('Not enough iners from TEC sample. Keeping all TEC samples for polynomial fitting.')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"iners"? Should it be "inliers"?


# Keep only inliers, but fall back to the plain fit if too few survive.
inliers = np.abs(resid) <= threshold
if np.count_nonzero(inliers) <= degree:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be "degrees + 1" instead? A polynomial with degree-n should require at least n + 1 samples.

Comment on lines +112 to +113
Multiplier `k` on the robust noise scale that sets the inlier
threshold: `k * MAD`. Default 1.5.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

k non longer needed?

zero_arr = np.zeros(tec_correction.data.shape)
tec_correction = isce3.core.LUT2d(tec_correction.x_axis,
tec_correction.y_axis,
zero_arr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zero_arr)
zero_arr, b_error=False)

Like @hfattahi mentioned, this would prevent an error occurring when our dummy extent isn't wide enough for whatever reason, replicating the behavior of LUT2d() (no arg ctor)

Sample values (the TEC profile) to fit.
degree: int
Degree of the polynomial to fit.
sigma: float

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sigma: float
num_sigma: float

Name is a little confusing since it's a sigma multiplier (unitless) rather than something with the units of the data.

# Degenerate noise scale (near-perfect fit); nothing to reject.
if not np.isfinite(threshold) or threshold <= 0:
info_channel.log('TEC MAD Threshold is not valid. Keeping all TEC samples for polynomial fitting.')
return np.polyval(coeffs, x)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return np.polyval(coeffs, x)
raise RuntimeError()

If we get here I'm not sure it's safe to evaluate the polynomial. So I'd suggest either return y or just throw an exception.

inliers = np.abs(resid) <= threshold
if np.count_nonzero(inliers) <= degree:
info_channel.log('Not enough iners from TEC sample. Keeping all TEC samples for polynomial fitting.')
return np.polyval(coeffs, x)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return np.polyval(coeffs, x)
return y

This condition suggests the fit is bad, so maybe it's better not to apply polynomial smoothing and just return the data. Should mention the behavior in the docstring.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I'm having some doubts about what to do in this case.

return delta_r


def _mad_polyfit(x: np.ndarray, y: np.ndarray, degree: int,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _mad_polyfit(x: np.ndarray, y: np.ndarray, degree: int,
def _smooth_mad_polyfit(x: np.ndarray, y: np.ndarray, degree: int,

The function doesn't return fit coefficients like numpy.polyfit, so I think it's clearer if the name is more different. The function performs smoothing, so _smooth_mad_polyfit makes more sense to me, at least.

apply_azimuth_correction: True
use_total_tec_only: False
ignore_invalid_topside_tec: True
polyfit_tec_profile: False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
polyfit_tec_profile: False
polyfit_tec_profile: False
num_sigma: 1.5

apply_azimuth_correction: True
use_total_tec_only: False
ignore_invalid_topside_tec: True
polyfit_tec_profile: False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
polyfit_tec_profile: False
polyfit_tec_profile: False
num_sigma: 1.5

tec_correction_options:
use_total_tec_only: bool(required=False)
ignore_invalid_topside_tec: bool(required=False)
polyfit_tec_profile: bool(required=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
polyfit_tec_profile: bool(required=False)
polyfit_tec_profile: bool(required=False)
num_sigma: num(min=0.0, required=False)

apply_azimuth_correction: bool(required=False)
use_total_tec_only: bool(required=False)
ignore_invalid_topside_tec: bool(required=False)
polyfit_tec_profile: bool(required=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
polyfit_tec_profile: bool(required=False)
polyfit_tec_profile: bool(required=False)
num_sigma: num(min=0.0, required=False)

apply_azimuth_correction: bool(required=False)
use_total_tec_only: bool(required=False)
ignore_invalid_topside_tec: bool(required=False)
polyfit_tec_profile: bool(required=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
polyfit_tec_profile: bool(required=False)
polyfit_tec_profile: bool(required=False)
num_sigma: num(min=0.0, required=False)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants