Skip to content

Decimate inputDataExceptionMask when bandpassing mixed-mode RSLCs - #409

Merged
xhuang-jpl merged 2 commits into
isce-framework:developfrom
xhuang-jpl:fix_mixed_20_40
Oct 7, 2026
Merged

xhuang-jpl merged 2 commits into
isce-framework:developfrom
xhuang-jpl:fix_mixed_20_40

Conversation

@xhuang-jpl

Copy link
Copy Markdown
Contributor

Mixed-mode (20/40 MHz) InSAR crashed in prepare_insar_hdf5 with:

ValueError: inputDataExceptionMask shape (54720, 52582) differs from
            the swath shape (54720, 26291)

bandpass_insar copies the RSLC tree verbatim with cp_h5_meta_data, then
rewrites the polarization rasters, validSamplesSubSwath* and slantRange
onto the decimated range grid — but never inputDataExceptionMask. The mask
was left on the pre-bandpass grid at twice the width of the swath it
describes. The shape check in _RSLCInputDataExceptionMask surfaced a real
inconsistency, not a spurious assertion.

Two fixes:

1. Decimate the mask onto the bandpassed grid. New
decimate_input_data_exception_mask maps input sample i to output sample
i // decimation_factor — the same mapping validSamplesSubSwath{i} is
rescaled by — and OR-reduces every bit of each group, so no set bit is lost.
That is the aggregation the dataset is defined by ("bitwise OR of input data
exception codes ... also includes OR of validity mask bits") and applies
unchanged to the 8-bit mask (anomaly codes only) and the 16-bit mask (plus
per-polarization validity bits in the high byte). The output width is read
from the bandpassed SLC rather than recomputed, so it cannot disagree with
the rasters; dtype, chunking, compression and attributes are preserved, and
the trailing samples dropped are exactly those bandpass_shift_spectrum
trims from the SLC (verified bandpassed_samples * factor == resample_width_end over factors 2–8 × widths 1–200 plus the real widths).

2. Fix a pre-existing off-by-one in the validSamplesSubSwath rescaling.
bandpass_ratio is 0.499999999996329, not 0.5, because its numerator and
denominator are accumulated separately (3.1228381041437387 vs
6.245676208333333). int(i * ratio) therefore loses a pixel on every
even index — the exact i/2 is a whole number that any downward error
drops below, while odd indices land on x.5 and survive. On the test data
the sub-swath start went 1602 → 800 instead of 801, claiming two
out-of-swath samples as valid. Both paths now share one integer
decimation_factor and the identical i // decimation_factor mapping.

Test plan

  • New test_decimate_input_data_exception_mask: covers uint8/uint16,
    bit preservation across the full dtype width, width taken from the SLC,
    the already-on-grid no-op, and the too-narrow-mask ValueError
  • pytest tests/python/packages/nisar/workflows/bandpass_insar.py — 3 passed
  • test_bandpass_run independently reproduces the off-by-one on committed
    data ([200,200] → [99,99] before, [100,100] after) with a different
    ratio error, so it is not specific to one product
  • Full nisar.workflows.insar on a real 20/40 MHz QPDH/DHDH pair:
    prepare_insar_hdf5 completes (115 s, previously crashed); bandpassed
    HH, inputDataExceptionMask and slantRange all agree at
    (54720, 26291); decimation is a bit-exact OR-reduction of the source

Comment thread python/packages/nisar/workflows/bandpass_insar.py
mask_path = f"{freq_path}/inputDataExceptionMask"
if mask_path not in src_h5:
return

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.

you may want to add a sanity check to ensure decimation_factor >= 1, and I assume if decimation_factor == 1 you would return without doing anything.

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.

Yes, similar to @gshiroma's suggestion that we add the code below to handle the case that decimation_factor is equal to 0

            # Handle the case when the decimation_factor == 0
            decimation_factor = max(decimation_factor, 1)

@hfattahi hfattahi added this to the R05.03.2 milestone Oct 6, 2026
Comment on lines +287 to +289
decimation_factor = int(np.round(
bandpass_meta['range_spacing'] /
target_meta_data.rg_pxl_spacing))

@gshiroma gshiroma Oct 6, 2026 •

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
decimation_factor = int(np.round(
bandpass_meta['range_spacing'] /
target_meta_data.rg_pxl_spacing))
decimation_factor = max(int(np.round(
bandpass_meta['range_spacing'] /
target_meta_data.rg_pxl_spacing)), 1)

To handle the case when decimation_factor = 0.

@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.

LGTM


src_mask = src_h5[mask_path]
lines, samples = src_mask.shape
if samples == bandpassed_samples:

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
if samples == bandpassed_samples:
if samples == bandpassed_samples:
if decimation_factor != 1:
raise ValueError("mismatch between decimation factor and image size")
# else earlier code has already copied mask, so nothing to do.

Should have a sanity check here in the no-op case.

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.

Fixed. Thanks


attrs = dict(src_mask.attrs)
del dst_h5[mask_path]
dst_mask = dst_h5.create_dataset(

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.

I wonder if we need to specify the HDF5 missing data value here as well (not the _FillValue attribute). I'll have to look up how to do this using h5py.

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.

The create_dataset method has the fillvalue keyword (h5py API) to specify the HDF5-level fill value for uninitialized data (C or C++ API). I don't think we're actually doing this explicitly anywhere in ISCE3 yet, but recent benchmarking shows it could be important. In this case, the optimal thing to do would be to

  • Set fillvalue at dataset creation time.
  • Write the entire dataset in one sequential pass over chunks (without writing anything else to the HDF5 file).
    • This stores the mask in as few pages as possible.
  • Skip writing any chunks where all pixels are equal to fillvalue.
    • Minimizes the amount of chunk metadata stored to disk.
    • Allows HDF5 to trivially memset the chunk on read instead of decompressing, which is around 30x faster.

I don't think this necessarily has to be implemented for this PR, but I expect we may expend some effort implementing this strategy for many products/datasets in the future.

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.

Thanks @bhawkins for this. It might be better that we open an issue for this since once this PR is merged, we might forget this.

# Equals the resample_width_end that bandpass_shift_spectrum trims the
# SLC to, so the mask drops the same trailing samples as the rasters
samples_used = bandpassed_samples * decimation_factor
if samples_used > samples:

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.

Typically decimation might end up with the case samples_used up to samples + decimation_factor - 1, and this is a stronger assumption. Seems okay with me since it's in the docstring.

@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.

LGTM.

@xhuang-jpl
xhuang-jpl merged commit ba0f996 into isce-framework:develop Oct 7, 2026
9 checks passed
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.

4 participants