Skip to content

Fix NaN and bias in perpendicular baseline near zero - #754

Open
mgovorcin wants to merge 2 commits into
isce-framework:mainfrom
mgovorcin:fix/baseline-near-zero
Open

mgovorcin wants to merge 2 commits into
isce-framework:mainfrom
mgovorcin:fix/baseline-near-zero

Conversation

@mgovorcin

Copy link
Copy Markdown
Contributor

Change Summary

baseline.compute got the perpendicular baseline from the two ranges by the law of cosines, then sin_theta = np.sqrt(1 - cos_theta**2). That fails in two ways:

  1. NaN where B⊥ crosses zero. With the baseline nearly parallel to the look, rounding pushes |cos_theta| just above 1 and the square root returns NaN. In DISP-S1 products this shows up as a NaN band in corrections/perpendicular_baseline along the B⊥ = 0 line. In one set of 376 archived products, 15 have it, 20k–2.7M pixels each.
  2. Bias away from zero. compute_baselines (and disp-s1's _baselines.compute_baselines) removes the along-track offset from the secondary position, but passes the secondary range measured before that shift. On S1A–S1B pairs this is a 0.1–0.5 mm range mismatch, which the law of cosines turns into centimetres of B⊥ error near zero: up to 19 cm at a 32 m along-track shift.

The fix computes B⊥ directly as the baseline component perpendicular to the unit look vector, |B × l̂|. It doesn't use the ranges, so neither problem can occur. The sign rule is unchanged. ref_range / sec_range stay in the signature, now unused, because disp-s1 calls dolphin.baseline.compute with them positionally.

Away from zero nothing changes: on tests/data/baseline_cslcs (B⊥ ≈ 135–142 m) the new values are within 0.04 mm of the old ones.

Verification on archived DISP-S1 products

For each of the 15 affected products, B⊥ was recomputed through disp-s1's own code (disp_s1._baselines.compute_baselines on the product grid every 50th pixel, then _interpolate_data, as in product.py). The inputs were the first reference and secondary CSLC by name from ASF, run once with this branch and once with main:

  • With main the recomputation reproduces every archived layer: the same NaN pixels and values within 0.03 mm. So the NaNs come from this function and not from anything downstream.
  • With this branch all 15 have 0 NaN.
  • Given ranges consistent with the shifted secondary position, the old formula equals the new one to within 1e-5 m (usually 1e-7 m) at 400 points per product. So the remaining differences are the old formula's bias, not a change in definition.

archived vs fix

Top: an S1A–S1A pair, where the NaN band disappears. Bottom: an S1A–S1B pair with a 21–32 m along-track shift, where the band disappears and the bias away from the zero line is removed too. In the figure titles, "where |B⊥| > 0.5 m" is measured on the fixed layer; in the table below it's measured on the archived layer.

Frame Reference → secondary NaN px, archived NaN px, old code rerun NaN px, fix Along-track shift removed (max) Fix − archived where |B⊥| > 0.5 m
F03848 20170827 → 20171026 1,026,502 1,026,502 0 14.0 m 0.75 cm
F09154 20160705 → 20160729 2,336,590 2,336,590 0 18.6 m 0.72 cm
F09154 20160705 → 20170101 176,955 176,955 0 13.4 m 1.33 cm
F09154 20160705 → 20170119 2,653,491 2,653,491 0 32.1 m 7.51 cm
F16943 20170906 → 20180715 523,506 523,506 0 1.5 m 0.01 cm
F23443 20170516 → 20170925 20,351 20,351 0 1.1 m 0.00 cm
F24196 20160720 → 20161129 817,400 817,400 0 5.6 m 0.28 cm
F34482 20160711 → 20160828 312,504 312,504 0 4.3 m 0.02 cm
F34482 20171010 → 20171115 1,224,014 1,224,014 0 14.4 m 0.22 cm
F34482 20171010 → 20180102 461,156 461,156 0 21.0 m 0.46 cm
F34732 20170314 → 20171121 140,400 140,400 0 1.4 m 0.01 cm
F44329 20160714 → 20161223 55,450 55,450 0 5.7 m 0.11 cm
F44329 20160714 → 20170116 221,000 221,000 0 2.9 m 0.01 cm
F44329 20160714 → 20170609 2,196,815 2,196,815 0 19.8 m 0.62 cm
F44329 20170609 → 20171112 2,287,969 2,287,969 0 24.2 m 3.65 cm

Same comparison on one burst (t035_073233_iw3, 20160705 → 20160729), on the CSLC lon/lat grid:

burst before after

How to verify

pytest tests/test_baseline.py

The new tests put the secondary 100 m or 1000 m along the look and 10 m to 0 m across it, and check that |B⊥| is recovered to 1e-6 m and is finite. On main they fail at 0.01 m and 0 m. A second test checks that the sign flips with the side.

Pre-flight: pre-commit run --all-files clean. pytest: 781 tests run (tests/test_io_paths.py skipped, moto not installed): 757 passed, 21 skipped, 24 failed. The same 24 also fail on main in that environment (phass not installed, gdal_merge.py not on PATH), so none come from this change.

Not in this PR

  • Products already in the archive keep their NaN bands and bias until they're reprocessed with a release that includes this.
  • test_baseline_compute has two bare pytest.approx(...) lines that never assert, and its expected minimum (134.98) doesn't match what the function returns on main (134.51). That needs a separate fix.

Related issue number

None.

Checklist

  • The pull request title is a good summary of the changes - it will be used in the changelog
  • Unit tests for the changes exist
  • Tests pass on CI
  • Documentation reflects the changes where applicable
  • My PR is ready to review

🤖 Generated with Claude Code

mgovorcin and others added 2 commits November 4, 2026 21:04
The law of cosines on the two ranges returned NaN where the baseline is nearly
parallel to the look (|cos| > 1), and was biased by the range of the secondary
taken before its along-track shift was removed. The ranges are kept in the
signature for callers such as disp-s1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant