Improvement for TEC correction - #369
seongsujeong wants to merge 12 commits into
Conversation
…pside TEC rejection logic
hfattahi
left a comment
There was a problem hiding this comment.
some comments up to what we reviewed today.
| # 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, |
bhawkins
left a comment
There was a problem hiding this comment.
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.
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
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the updates. Just some minor comments.
| return delta_r | ||
|
|
||
|
|
||
| def _ransac_polyfit(x: np.ndarray, y: np.ndarray, degree: int, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
After testing and tuning the algorithm, it look like MAD-based filtering gives better results. I changed the implementation to MAD-based.
| sigma: float=1.5) -> np.ndarray: | ||
| ''' | ||
| Robustly fit a polynomial to a TEC profile by rejecting noisy samples using | ||
| a MAD-based threshold. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.') |
There was a problem hiding this comment.
"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: |
There was a problem hiding this comment.
Shouldn't it be "degrees + 1" instead? A polynomial with degree-n should require at least n + 1 samples.
| Multiplier `k` on the robust noise scale that sets the inlier | ||
| threshold: `k * MAD`. Default 1.5. |
| zero_arr = np.zeros(tec_correction.data.shape) | ||
| tec_correction = isce3.core.LUT2d(tec_correction.x_axis, | ||
| tec_correction.y_axis, | ||
| zero_arr) |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
| 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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
| polyfit_tec_profile: bool(required=False) | |
| polyfit_tec_profile: bool(required=False) | |
| num_sigma: num(min=0.0, required=False) |
This PR adds several options to ionospheric TEC correction
EDIT
09/10/2026: RANSAC-based outlier detection is implemented and being applied before polynomial fitting. Test is ongoing internally.