Skip to content

Add radar wavelength reader for NISAR GSLC products - #225

Open
s-sasaki-earthsea-wizard wants to merge 3 commits into
opera-adt:mainfrom
s-sasaki-earthsea-wizard:feature/nisar-gslc-wavelength
Open

Add radar wavelength reader for NISAR GSLC products#225
s-sasaki-earthsea-wizard wants to merge 3 commits into
opera-adt:mainfrom
s-sasaki-earthsea-wizard:feature/nisar-gslc-wavelength

Conversation

@s-sasaki-earthsea-wizard

Copy link
Copy Markdown

Summary

NISAR GSLC products state their radar center frequency per frequency sub-band, in centerFrequency, but opera_utils has no reader for it.
This adds one:

  • opera_utils.nisar.get_nisar_wavelength(filename, subdataset) — resolves the frequency group from the subdataset path and returns SPEED_OF_LIGHT / centerFrequency.
  • GslcProduct.get_wavelength(frequency="A") — a thin convenience wrapper for when you have a product object rather than a subdataset path.

Both work on local files and remote (HTTPS/S3) URLs through open_h5. This is a pure addition: no existing function or signature changes.

Motivation

Downstream, dolphin cannot auto-detect the radar wavelength for NISAR GSLC inputs (isce-framework/dolphin#704); its wavelength stays unset and timeseries/ outputs silently remain in radians. In that issue, the suggestion was to put the metadata reader in opera_utils rather than in dolphin, since this is exactly the kind of product-metadata access opera_utils.nisar already owns.
This PR is that reader.

A tempting shortcut is a hardcoded L-band constant (or a mode→frequency lookup table keyed on the filename). Real products rule that out. Across the NISAR_L2_GSLC_BETA_V1 collection we measured six distinct center frequencies — 1221.5, 1229.0, 1236.5, 1239.0, 1257.5, 1293.5 MHz — a wavelength spread of ~13.7 mm (5.9% of a wavelength). No single constant is correct, and many granules carry both frequencyA and frequencyB with two different center frequencies in one file, so the filename alone cannot say which applies.
The product states the value exactly; the right move is to read it.

Design

  • The frequency group is located from the subdataset path, not guessed.
    Reading the subdataset .../frequencyA/HH reads .../frequencyA/centerFrequency from the same group. This makes the wavelength always match the radar band and sub-band of the data actually being processed, and it removes any LSAR/SSAR
    ambiguity structurally (the band is part of the path).
  • Never falls back to another sub-band. The core reader never guesses: its sub-band comes from the subdataset path (exactly one frequencyA/frequencyB segment required). The convenience method follows the existing GslcProduct
    convention of defaulting its frequency argument to "A", but it never falls back to another available sub-band — on a frequencyB-only product, omitting the argument raises rather than silently returning B. This matters because A and B can differ by ~2.8% in wavelength, and that error would propagate directly into displacement.
  • Fail loud, with context. Missing or invalid centerFrequency (non-scalar, non-real dtype, non-finite, ≤ 0) raises a ValueError naming the dataset path and the file, rather than returning a plausible-but-wrong number.

Testing

  • Unit tests (tests/test_nisar_wavelength.py, 27 tests): synthetic GSLC HDF5 built with h5py, covering subdataset path resolution (frequencyA/B, the group itself, SSAR paths, reading the subdataset's own group rather than the
    first frequency group), path validation (missing/multiple frequency segments, frequencyC, frequencyAB), centerFrequency validation (positive integer dtype accepted; missing, group-instead-of-dataset, non-scalar, complex/bool/ string dtypes, zero, negative, NaN, ±inf rejected with filename + path in the message), a known-value check (a 1,257.5 MHz center frequency ≈ 23.84 cm), and the GslcProduct delegation including a frequencyB-only product where the default "A" correctly raises.
  • Real-data validation: run against 13 real GSLC granules (one per
    (mode, polarization) configuration) via remote byte-range reads — no bulk download. All 22 present sub-bands returned exactly c / centerFrequency (bit-identical), all 4 absent sub-bands raised as designed, and the product method matched. Reproducible with make validate-wavelength; the full report (pinned) is here, and the harness lives in the same repository.

API policy question

Currently the reader raises ValueError in two distinct situations: (a) the requested sub-band is genuinely absent from the product, and (b) the sub-band is present but its centerFrequency metadata is invalid. Case (b) — present but
invalid — should always raise, and I am not proposing to change it.

The one open choice is case (a): would maintainers prefer get_nisar_wavelength / get_wavelength to return None for a genuinely absent requested sub-band, so the caller decides what to do?
The current raise-on-absent keeps a float-only return type and surfaces product/request mismatches loudly; returning None would change the public contract to float | None.
Happy to go either way — flagging it here since it is the one user-visible policy decision in the PR.

Disclosure

These features, investigations, and tests were developed with AI assistance — Claude Opus 4.8 and Fable 5 (Anthropic), and GPT-5.6 Terra (OpenAI). All code was reviewed by the author and validated against an actual NISAR sample product.*

s-sasaki-earthsea-wizard and others added 3 commits July 17, 2026 20:38
Add `opera_utils.nisar.get_nisar_wavelength(filename, subdataset)`,
which resolves the frequency group from the subdataset path (so the
wavelength always matches the radar band and sub-band of the data being
processed), reads `centerFrequency`, and converts it via SPEED_OF_LIGHT.
Also add a thin `GslcProduct.get_wavelength(frequency=...)` delegating
method. Both work on local files and remote URLs through `open_h5`.

Missing or invalid (non-scalar, non-real, non-finite, <= 0)
`centerFrequency` values raise a contextual ValueError instead of
falling through silently.

Design discussion: isce-framework/dolphin#704

🤖 Assisted by [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Cover `get_nisar_wavelength` and `GslcProduct.get_wavelength` with
synthetic HDF5 files built by a factory fixture:

- subdataset path resolution: frequencyA/B datasets, the frequency group
  itself, SSAR paths (band-agnostic), and reading from the subdataset's
  parent group rather than the first frequency group in the file
- path validation: missing/multiple frequency segments, frequencyC, and
  longer segment names like frequencyAB are rejected
- centerFrequency validation: positive integer dtypes accepted; missing,
  group-instead-of-dataset, non-scalar, complex/bool/string dtypes,
  zero, negative, NaN, and +/-inf rejected with the filename and dataset
  path included in the error message
- known value: NISAR L-band 20 MHz mode resolves to ~23.84 cm
- product method: default frequency, explicit "B", invalid frequency,
  and a frequencyB-only product

🤖 Assisted by [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@s-sasaki-earthsea-wizard

Copy link
Copy Markdown
Author

Gentle ping on this — no rush if the review queue is deep. If you'd like any changes (API naming, scope, test layout), let me know and I'll turn them around quickly. Happy to rebase onto current main if that helps.

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