Skip to content

Allow interpolation across narrow masked water bodies - #94

Draft
scottstanie wants to merge 2 commits into
mainfrom
codex/interpolate-masked-water
Draft

Allow interpolation across narrow masked water bodies#94
scottstanie wants to merge 2 commits into
mainfrom
codex/interpolate-masked-water

Conversation

@scottstanie

Copy link
Copy Markdown
Owner

What changed

Adds opt-in unwrap(interp_across_mask=True), CLI --interp-across-mask, and benchmark plumbing. Exact-zero masked pixels can receive unit-amplitude phase only when a high-weight neighbor exists inside interp_max_radius. Filled pixels join the temporary solve mask but remain masked in returned phase and connected components; unsupported ocean remains nodata.

Result and tradeoff

On 008_055_D_073 with bridging disabled:

  • no interpolation: 99.8400% per-component, 10.9 s
  • across-mask interpolation: 99.9993%, 67–100 s

The normal bridge path is already 99.9813% in 11.5 s. This PR is therefore an orthogonal research/A-B option, not a recommended default or campaign setting.

Validation

  • synthetic no-bridge river test proves the temporary phase levels the banks
  • real 008_055 no-bridge A/B
  • full stacked Rust/Python test suites

scottstanie and others added 2 commits July 20, 2026 15:05
…region

Interpolating over a narrow river was already the intent - "ignore little water
bodies and interpolate a smooth thing from one to the next" - but two things in
our own pipeline prevented it. `interpolate` skips any pixel whose complex value
is exactly zero (the `ifg != 0` nodata convention it shares with dolphin), and
compare_gunw zeroes masked pixels before calling unwrap, so water was invisible
to the interpolator; unwrap then re-zeroed masked pixels afterwards anyway.
Dolphin uses the same convention but is handed a raw interferogram where water
still carries noisy non-zero values, so it does interpolate there - the
difference was ours, not inherent.

`fill_invalid` makes those nodata pixels interpolation targets. They get UNIT
amplitude, because they have none to preserve and would otherwise come back as
0+0j and read as invalid to every downstream nodata test. The fill is
self-limiting: a pixel is only filled when the spiral search finds high-weight
support within interp_max_radius, so a narrow river fills while the middle of a
wide ocean finds nothing and stays nodata.

`unwrap(interp_across_mask=True)` wires that through and keeps the filled phase
for the solve. Masked pixels are never used as interpolation neighbours, and the
solver mask admits only water that was actually filled. The output contract is
unchanged: the returned phase and connected components are still masked, and
integration already crossed masked regions, so the filled path only serves to
level the two banks against each other.

On 008_055_D_073, the river frame the bridge fix rescued, it reaches 1.0000
per-component agreement with NO bridging at all (bridge alone: 0.9998; neither:
0.9984), so it does address the same failure from the other end. It is ~5x
slower than bridging on that frame (67s vs 12s), so this is an alternative to
evaluate rather than a replacement to adopt. Off by default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base automatically changed from codex/cryo-slope-guard to main July 21, 2026 12:20
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