From 40619b845c7f9d211273d7a5cb600a10705ad844 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Tue, 4 Mar 2025 18:11:59 -0800 Subject: [PATCH 1/9] Add ionosphere stack file to prep_aria arguments --- src/mintpy/load_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mintpy/load_data.py b/src/mintpy/load_data.py index 0452f1637..4753b69f4 100644 --- a/src/mintpy/load_data.py +++ b/src/mintpy/load_data.py @@ -740,6 +740,7 @@ def prepare_metadata(iDict): '--incidence-angle' : 'mintpy.load.incAngleFile', '--azimuth-angle' : 'mintpy.load.azAngleFile', '--water-mask' : 'mintpy.load.waterMaskFile', + '--iono' : 'mintpy.load.ionUnwFile', } for arg_name, opt_name in ARG2OPT_DICT.items(): From 482e2d310e11e8143f8fbcab8070edf3e38a7fd6 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Tue, 4 Mar 2025 18:13:30 -0800 Subject: [PATCH 2/9] bug fix for loading ARIA generated ionosphere stack --- src/mintpy/prep_aria.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mintpy/prep_aria.py b/src/mintpy/prep_aria.py index b21fe6824..1f5f09638 100644 --- a/src/mintpy/prep_aria.py +++ b/src/mintpy/prep_aria.py @@ -654,8 +654,8 @@ def load_aria(inps): layer_name, _ = get_correction_layer(inps.ionoFile) - if run_or_skip(inps, ds_name_dict, out_file=inps.outfile[0]) == 'run': - outname = f'{out_dir}/ionStack.h5' + outname = f'{out_dir}/ionStack.h5' + if run_or_skip(inps, ds_name_dict, out_file=outname) == 'run': writefile.layout_hdf5( outname, From d9a2aa2558d92d50346c340f5ad0e64bc37f9d4f Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Mon, 18 May 2026 14:18:25 -0700 Subject: [PATCH 3/9] Add SSAR support to NISAR loader --- src/mintpy/cli/prep_nisar.py | 9 + src/mintpy/defaults/smallbaselineApp.cfg | 1 + src/mintpy/load_data.py | 3 +- src/mintpy/prep_nisar.py | 384 ++++++++++++++++------- 4 files changed, 291 insertions(+), 106 deletions(-) diff --git a/src/mintpy/cli/prep_nisar.py b/src/mintpy/cli/prep_nisar.py index c7bc22936..99c9198d4 100755 --- a/src/mintpy/cli/prep_nisar.py +++ b/src/mintpy/cli/prep_nisar.py @@ -58,6 +58,15 @@ def create_parser(subparsers=None): help="NISAR frequency to load: auto defaults to A (default: %(default)s).", ) + parser.add_argument( + "-ba", + "--band", + dest="sar_band", + choices=["LSAR", "SSAR"], + default="LSAR", + help="NISAR product family to load (default: %(default)s).", + ) + parser.add_argument( "-o", "--out-dir", diff --git a/src/mintpy/defaults/smallbaselineApp.cfg b/src/mintpy/defaults/smallbaselineApp.cfg index 59d2756f3..b2d8e9f0c 100644 --- a/src/mintpy/defaults/smallbaselineApp.cfg +++ b/src/mintpy/defaults/smallbaselineApp.cfg @@ -30,6 +30,7 @@ mintpy.load.processor = auto #[isce, aria, hyp3, gmtsar, snap, gamma, roi mintpy.load.autoPath = auto #[yes / no], auto for no, use pre-defined auto path mintpy.load.updateMode = auto #[yes / no], auto for yes, skip re-loading if HDF5 files are complete mintpy.load.compression = auto #[gzip / lzf / none / default], auto for default (none/lzf for stack/geometry). +mintpy.load.band = LSAR #[LSAR / SSAR], LSAR by default, NISAR only mintpy.load.frequency = auto #[auto / A / B], auto for A, NISAR only ##---------for ISCE only: mintpy.load.metaFile = auto #[path of common metadata file for the stack], i.e.: ./reference/IW1.xml, ./referenceShelve/data.dat diff --git a/src/mintpy/load_data.py b/src/mintpy/load_data.py index 1ec38a044..a5156a8d5 100644 --- a/src/mintpy/load_data.py +++ b/src/mintpy/load_data.py @@ -636,6 +636,7 @@ def prepare_metadata(iDict): dem_file = iDict['mintpy.load.demFile'] gunw_files = iDict['mintpy.load.unwFile'] water_mask = iDict['mintpy.load.waterMaskFile'] + band = iDict.get('mintpy.load.band', 'LSAR') frequency = iDict.get('mintpy.load.frequency', 'auto') if str(dem_file).lower() in ['auto', 'none', 'no', '']: @@ -655,7 +656,7 @@ def prepare_metadata(iDict): ) # run prep_*.py - iargs = ['-i', gunw_files, '-d', dem_file, '--frequency', frequency] + iargs = ['-i', gunw_files, '-d', dem_file, '--band', band, '--frequency', frequency] if str(water_mask).lower() not in ['auto', 'none', 'no', ''] and os.path.exists(water_mask): iargs = iargs + ['--mask', water_mask] diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index 629c1c945..e9219347d 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -29,26 +29,6 @@ "frequencyA": "frequencyA", "frequencyB": "frequencyB", } -IDENTIFICATION = "/science/LSAR/identification" -RADARGRID_ROOT = "science/LSAR/GUNW/metadata/radarGrid" - -PROCESSINFO = { - "orbit_direction": f"{IDENTIFICATION}/orbitPassDirection", - "platform": f"{IDENTIFICATION}/missionId", - "start_time": f"{IDENTIFICATION}/referenceZeroDopplerStartTime", - "end_time": f"{IDENTIFICATION}/referenceZeroDopplerEndTime", - "rdr_xcoord": f"{RADARGRID_ROOT}/xCoordinates", - "rdr_ycoord": f"{RADARGRID_ROOT}/yCoordinates", - "rdr_slant_range": f"{RADARGRID_ROOT}/referenceSlantRange", - "rdr_height": f"{RADARGRID_ROOT}/heightAboveEllipsoid", - "rdr_incidence": f"{RADARGRID_ROOT}/incidenceAngle", - "rdr_los_x": f"{RADARGRID_ROOT}/losUnitVectorX", - "rdr_los_y": f"{RADARGRID_ROOT}/losUnitVectorY", - "rdr_wet_tropo": f"{RADARGRID_ROOT}/wetTroposphericPhaseScreen", - "rdr_hs_tropo": f"{RADARGRID_ROOT}/hydrostaticTroposphericPhaseScreen", - "rdr_SET": f"{RADARGRID_ROOT}/slantRangeSolidEarthTidesPhase", - "bperp": f"{RADARGRID_ROOT}/perpendicularBaseline", -} STACK_TYPES = {"ifgram", "ion", "tropo", "set"} @@ -67,25 +47,71 @@ def _normalize_frequency(frequency) -> str: return normalized -def _dataset_root_unw(frequency: str) -> str: - return f"/science/LSAR/GUNW/grids/{frequency}/unwrappedInterferogram" +def _normalize_sar_band(sar_band) -> str: + """Return the normalized NISAR product family name.""" + normalized = "LSAR" if sar_band is None else str(sar_band).upper() + if normalized not in {"LSAR", "SSAR"}: + raise ValueError("sar_band must be one of: LSAR, SSAR") + return normalized + + +def _science_root(sar_band: str = "LSAR") -> str: + return f"/science/{_normalize_sar_band(sar_band)}" + + +def _identification_root(sar_band: str = "LSAR") -> str: + return f"{_science_root(sar_band)}/identification" + + +def _radargrid_root(sar_band: str = "LSAR") -> str: + return f"{_science_root(sar_band)}/GUNW/metadata/radarGrid" + + +def _processinfo(sar_band: str = "LSAR") -> dict: + identification = _identification_root(sar_band) + radargrid_root = _radargrid_root(sar_band) + return { + "orbit_direction": f"{identification}/orbitPassDirection", + "platform": f"{identification}/missionId", + "start_time": f"{identification}/referenceZeroDopplerStartTime", + "end_time": f"{identification}/referenceZeroDopplerEndTime", + "ref_start_time": f"{identification}/referenceZeroDopplerStartTime", + "sec_start_time": f"{identification}/secondaryZeroDopplerStartTime", + "rdr_xcoord": f"{radargrid_root}/xCoordinates", + "rdr_ycoord": f"{radargrid_root}/yCoordinates", + "rdr_slant_range": f"{radargrid_root}/referenceSlantRange", + "rdr_height": f"{radargrid_root}/heightAboveEllipsoid", + "rdr_incidence": f"{radargrid_root}/incidenceAngle", + "rdr_los_x": f"{radargrid_root}/losUnitVectorX", + "rdr_los_y": f"{radargrid_root}/losUnitVectorY", + "rdr_wet_tropo": f"{radargrid_root}/wetTroposphericPhaseScreen", + "rdr_hs_tropo": f"{radargrid_root}/hydrostaticTroposphericPhaseScreen", + "rdr_SET": f"{radargrid_root}/slantRangeSolidEarthTidesPhase", + "bperp": f"{radargrid_root}/perpendicularBaseline", + } + +def _dataset_root_unw(frequency: str, sar_band: str = "LSAR") -> str: + return f"{_science_root(sar_band)}/GUNW/grids/{frequency}/unwrappedInterferogram" -def _parameters_root(frequency: str) -> str: + +def _parameters_root(frequency: str, sar_band: str = "LSAR") -> str: return ( - "/science/LSAR/GUNW/metadata/processingInformation/parameters/" + f"{_science_root(sar_band)}/GUNW/metadata/processingInformation/parameters/" f"unwrappedInterferogram/{frequency}" ) -def _center_frequency_path(frequency: str) -> str: - return f"/science/LSAR/GUNW/grids/{frequency}/centerFrequency" +def _center_frequency_path(frequency: str, sar_band: str = "LSAR") -> str: + return f"{_science_root(sar_band)}/GUNW/grids/{frequency}/centerFrequency" -def _datasets_for_pol(polarization: str, frequency: str) -> dict: +def _datasets_for_pol( + polarization: str, frequency: str, sar_band: str = "LSAR" +) -> dict: """Return per-call dataset paths for the selected frequency/polarization.""" - root = _dataset_root_unw(frequency) - parameters = _parameters_root(frequency) + root = _dataset_root_unw(frequency, sar_band) + parameters = _parameters_root(frequency, sar_band) return { "xcoord": f"{root}/{polarization}/xCoordinates", "ycoord": f"{root}/{polarization}/yCoordinates", @@ -97,22 +123,24 @@ def _datasets_for_pol(polarization: str, frequency: str) -> dict: "epsg": f"{root}/{polarization}/projection", "xSpacing": f"{root}/{polarization}/xCoordinateSpacing", "ySpacing": f"{root}/{polarization}/yCoordinateSpacing", - "polarization": f"/science/LSAR/GUNW/grids/{frequency}/listOfPolarizations", + "polarization": f"{_science_root(sar_band)}/GUNW/grids/{frequency}/listOfPolarizations", "range_look": f"{parameters}/numberOfRangeLooks", "azimuth_look": f"{parameters}/numberOfAzimuthLooks", } -def _resolve_frequency(gunw_file: str, frequency, polarization: str) -> str: +def _resolve_frequency( + gunw_file: str, frequency, polarization: str, sar_band: str = "LSAR" +) -> str: """Resolve and validate the requested NISAR frequency.""" resolved = _normalize_frequency(frequency) - datasets = _datasets_for_pol(polarization, resolved) + datasets = _datasets_for_pol(polarization, resolved, sar_band) with h5py.File(gunw_file, "r") as ds: required_paths = [ - _dataset_root_unw(resolved), + _dataset_root_unw(resolved, sar_band), datasets["unw"], - _center_frequency_path(resolved), + _center_frequency_path(resolved, sar_band), ] missing = [path for path in required_paths if path not in ds] @@ -130,8 +158,9 @@ def _resolve_frequency(gunw_file: str, frequency, polarization: str) -> str: "Check that the input file contains frequencyB for this polarization." ) raise ValueError( - f"NISAR {requested} data for polarization {polarization!r} was not found " - f"in {gunw_file}. Missing path: {missing[0]}. {hint}" + f"NISAR {_normalize_sar_band(sar_band)} {requested} data for " + f"polarization {polarization!r} was not found in {gunw_file}. " + f"Missing path: {missing[0]}. {hint}" ) return resolved @@ -256,9 +285,15 @@ def _coerce_subset_metadata_types(meta): return meta -def _read_unwrapped_phase_valid_mask(gunw_file: str, xybbox, pol: str, frequency: str): +def _read_unwrapped_phase_valid_mask( + gunw_file: str, + xybbox, + pol: str, + frequency: str, + sar_band: str = "LSAR", +): """Fallback validity mask based on finite unwrappedPhase (+ _FillValue check).""" - datasets = _datasets_for_pol(pol, frequency) + datasets = _datasets_for_pol(pol, frequency, sar_band) path = datasets["unw"] with h5py.File(gunw_file, "r") as ds: dset = ds[path] @@ -271,14 +306,22 @@ def _read_unwrapped_phase_valid_mask(gunw_file: str, xybbox, pol: str, frequency return valid -def _read_is_land_and_valid_mask(gunw_file: str, xybbox, pol: str, frequency: str): +def _read_is_land_and_valid_mask( + gunw_file: str, + xybbox, + pol: str, + frequency: str, + sar_band: str = "LSAR", +): """Decode the native GUNW mask into MintPy's keep-mask convention.""" - datasets = _datasets_for_pol(pol, frequency) + datasets = _datasets_for_pol(pol, frequency, sar_band) path = datasets["mask"] with h5py.File(gunw_file, "r") as ds: if path not in ds: - return _read_unwrapped_phase_valid_mask(gunw_file, xybbox, pol, frequency) + return _read_unwrapped_phase_valid_mask( + gunw_file, xybbox, pol, frequency, sar_band + ) dset = ds[path] mask_bits = dset[xybbox[1] : xybbox[3], xybbox[0] : xybbox[2]] @@ -297,15 +340,22 @@ def _read_is_land_and_valid_mask(gunw_file: str, xybbox, pol: str, frequency: st return is_valid & ~water_mask -def _read_common_is_land_and_valid_mask(input_files, bbox, pol: str, frequency: str): +def _read_common_is_land_and_valid_mask( + input_files, bbox, pol: str, frequency: str, sar_band: str = "LSAR" +): """Return the common keep-mask across all input GUNW products.""" common_mask = None for gunw_file in input_files: geo_ds = read_subset( - gunw_file, bbox, polarization=pol, frequency=frequency, geometry=True + gunw_file, + bbox, + polarization=pol, + frequency=frequency, + sar_band=sar_band, + geometry=True, ) mask = _read_is_land_and_valid_mask( - gunw_file, geo_ds["xybbox"], pol, frequency + gunw_file, geo_ds["xybbox"], pol, frequency, sar_band ).astype(bool, copy=False) if common_mask is None: @@ -336,13 +386,14 @@ def _apply_external_mask( external_mask_file, polarization: str, frequency: str, + sar_band: str = "LSAR", ): """Refine a keep-mask with an optional external raster mask.""" if not _external_mask_is_set(external_mask_file): return keep_mask dst_epsg, xcoord, ycoord = _read_target_grid( - gunw_file, xybbox, polarization, frequency + gunw_file, xybbox, polarization, frequency, sar_band ) mask_src_epsg = _read_raster_epsg(external_mask_file) external_mask = _warp_to_grid_mem( @@ -356,10 +407,13 @@ def _apply_external_mask( return keep_mask & external_mask -def _read_perpendicular_baseline(gunw_file: str) -> np.float32: +def _read_perpendicular_baseline( + gunw_file: str, sar_band: str = "LSAR" +) -> np.float32: """Read the NISAR perpendicular baseline as one finite mean value.""" + processinfo = _processinfo(sar_band) with h5py.File(gunw_file, "r") as ds: - dset = ds[PROCESSINFO["bperp"]] + dset = ds[processinfo["bperp"]] bperp = np.asarray(dset[()], dtype=np.float64).reshape(-1) fill = dset.attrs.get("_FillValue", None) @@ -376,9 +430,15 @@ def _read_perpendicular_baseline(gunw_file: str) -> np.float32: return np.float32(pbase) -def _read_target_grid(gunw_file: str, xybbox, polarization: str, frequency: str): +def _read_target_grid( + gunw_file: str, + xybbox, + polarization: str, + frequency: str, + sar_band: str = "LSAR", +): """Read the destination EPSG and subset grid axes from a GUNW file.""" - datasets = _datasets_for_pol(polarization, frequency) + datasets = _datasets_for_pol(polarization, frequency, sar_band) with h5py.File(gunw_file, "r") as ds: return ( int(ds[datasets["epsg"]][()]), @@ -387,27 +447,37 @@ def _read_target_grid(gunw_file: str, xybbox, polarization: str, frequency: str) ) -def _read_radar_grid_fields(gunw_file: str, field_map: dict): +def _read_radar_grid_fields( + gunw_file: str, field_map: dict, sar_band: str = "LSAR" +): """Read radar-grid interpolation axes plus the requested data fields.""" + processinfo = _processinfo(sar_band) rdr_coords = {} with h5py.File(gunw_file, "r") as ds: - rdr_coords["xcoord_radar_grid"] = ds[PROCESSINFO["rdr_xcoord"]][()] - rdr_coords["ycoord_radar_grid"] = ds[PROCESSINFO["rdr_ycoord"]][()] - rdr_coords["height_radar_grid"] = ds[PROCESSINFO["rdr_height"]][()] + rdr_coords["xcoord_radar_grid"] = ds[processinfo["rdr_xcoord"]][()] + rdr_coords["ycoord_radar_grid"] = ds[processinfo["rdr_ycoord"]][()] + rdr_coords["height_radar_grid"] = ds[processinfo["rdr_height"]][()] for out_key, process_key in field_map.items(): - rdr_coords[out_key] = ds[PROCESSINFO[process_key]][()] + rdr_coords[out_key] = ds[processinfo[process_key]][()] return rdr_coords def _prepare_radar_grid_interpolation( - gunw_file, dem_file, xybbox, polarization, frequency, field_map, valid_mask=None + gunw_file, + dem_file, + xybbox, + polarization, + frequency, + field_map, + valid_mask=None, + sar_band: str = "LSAR", ): """Build the common DEM/grid/valid-mask context for radar-grid interpolation.""" dem_src_epsg = _read_raster_epsg(dem_file) dst_epsg, xcoord, ycoord = _read_target_grid( - gunw_file, xybbox, polarization, frequency + gunw_file, xybbox, polarization, frequency, sar_band ) - rdr_coords = _read_radar_grid_fields(gunw_file, field_map) + rdr_coords = _read_radar_grid_fields(gunw_file, field_map, sar_band) dem_subset_array = _warp_to_grid_mem( src_path=dem_file, @@ -421,7 +491,7 @@ def _prepare_radar_grid_interpolation( y_2d, x_2d = np.meshgrid(ycoord, xcoord, indexing="ij") if valid_mask is None: valid_mask = _read_is_land_and_valid_mask( - gunw_file, xybbox, polarization, frequency + gunw_file, xybbox, polarization, frequency, sar_band ) else: valid_mask = np.asarray(valid_mask, dtype=np.bool_) @@ -501,26 +571,33 @@ def _resolve_stack_type(stack_type, outfile): ) -def _required_paths_for_stack_type(stack_type, polarization, frequency): +def _required_paths_for_stack_type( + stack_type, polarization, frequency, sar_band: str = "LSAR" +): """Return HDF5 source datasets needed to build the requested stack.""" - datasets = _datasets_for_pol(polarization, frequency) + datasets = _datasets_for_pol(polarization, frequency, sar_band) + processinfo = _processinfo(sar_band) if stack_type == "ifgram": return [datasets["unw"], datasets["cor"], datasets["connComp"]] if stack_type == "ion": return [datasets["ion"], datasets["cor"], datasets["connComp"]] if stack_type == "tropo": - return [PROCESSINFO["rdr_wet_tropo"], PROCESSINFO["rdr_hs_tropo"]] + return [processinfo["rdr_wet_tropo"], processinfo["rdr_hs_tropo"]] if stack_type == "set": - return [PROCESSINFO["rdr_SET"]] + return [processinfo["rdr_SET"]] raise ValueError( f"Unsupported stack_type {stack_type!r}; expected one of {sorted(STACK_TYPES)}" ) -def _missing_required_paths(inp_files, stack_type, polarization, frequency): +def _missing_required_paths( + inp_files, stack_type, polarization, frequency, sar_band: str = "LSAR" +): """Return missing required HDF5 source paths as (file, path) pairs.""" - required_paths = _required_paths_for_stack_type(stack_type, polarization, frequency) + required_paths = _required_paths_for_stack_type( + stack_type, polarization, frequency, sar_band + ) missing = [] for file in inp_files: @@ -530,13 +607,25 @@ def _missing_required_paths(inp_files, stack_type, polarization, frequency): return missing -def _read_stack_observation(file, stack_type, bbox, dem_file, polarization, frequency): +def _read_stack_observation( + file, + stack_type, + bbox, + dem_file, + polarization, + frequency, + sar_band: str = "LSAR", +): """Read one observation for the requested stack type.""" - pbase = _read_perpendicular_baseline(file) + pbase = _read_perpendicular_baseline(file, sar_band) if stack_type in {"ifgram", "ion"}: dataset = read_subset( - file, bbox, polarization=polarization, frequency=frequency + file, + bbox, + polarization=polarization, + frequency=frequency, + sar_band=sar_band, ) unwrap_key = "unw_data" if stack_type == "ifgram" else "ion_data" return { @@ -547,7 +636,12 @@ def _read_stack_observation(file, stack_type, bbox, dem_file, polarization, freq } geo_ds = read_subset( - file, bbox, polarization=polarization, frequency=frequency, geometry=True + file, + bbox, + polarization=polarization, + frequency=frequency, + sar_band=sar_band, + geometry=True, ) if stack_type == "tropo": unwrap_phase = read_and_interpolate_troposphere( @@ -556,6 +650,7 @@ def _read_stack_observation(file, stack_type, bbox, dem_file, polarization, freq geo_ds["xybbox"], polarization=polarization, frequency=frequency, + sar_band=sar_band, ) else: unwrap_phase = read_and_interpolate_SET( @@ -564,6 +659,7 @@ def _read_stack_observation(file, stack_type, bbox, dem_file, polarization, freq geo_ds["xybbox"], polarization=polarization, frequency=frequency, + sar_band=sar_band, ) return {"unwrap_phase": unwrap_phase, "pbase": pbase} @@ -632,18 +728,28 @@ def load_nisar(inps): # extract metadata pol = getattr(inps, "polarization", "HH") + sar_band = getattr(inps, "sar_band", "LSAR") frequency = _resolve_frequency( - input_files[0], getattr(inps, "frequency", "auto"), pol + input_files[0], getattr(inps, "frequency", "auto"), pol, sar_band ) - print(f"Using NISAR {frequency}") + print(f"Using NISAR {_normalize_sar_band(sar_band)} {frequency}") metadata, bounds = extract_metadata( - input_files, bbox=bbox, polarization=pol, frequency=frequency + input_files, + bbox=bbox, + polarization=pol, + frequency=frequency, + sar_band=sar_band, ) common_mask = _read_common_is_land_and_valid_mask( - input_files, bounds, pol=pol, frequency=frequency + input_files, bounds, pol=pol, frequency=frequency, sar_band=sar_band ) first_geo_ds = read_subset( - input_files[0], bounds, polarization=pol, frequency=frequency, geometry=True + input_files[0], + bounds, + polarization=pol, + frequency=frequency, + sar_band=sar_band, + geometry=True, ) common_mask = _apply_external_mask( common_mask, @@ -652,6 +758,7 @@ def load_nisar(inps): inps.mask_file, pol, frequency, + sar_band, ) print( "Common valid land pixels from all NISAR masks: " @@ -666,7 +773,7 @@ def load_nisar(inps): set_stack_file = os.path.join(inps.out_dir, "inputs/setStack.h5") # date pairs - date12_list = _get_date_pairs(input_files) + date12_list = _get_date_pairs(input_files, sar_band=sar_band) # geometry metadata = prepare_geometry( @@ -679,6 +786,7 @@ def load_nisar(inps): commonMask=common_mask, polarization=pol, frequency=frequency, + sar_band=sar_band, ) # standalone water mask (MintPy format) @@ -692,6 +800,7 @@ def load_nisar(inps): commonMask=common_mask, polarization=pol, frequency=frequency, + sar_band=sar_band, ) # ifgram stack @@ -704,6 +813,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, + sar_band=sar_band, stack_type="ifgram", commonMask=common_mask, ) @@ -718,6 +828,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, + sar_band=sar_band, stack_type="ion", commonMask=common_mask, ) @@ -732,6 +843,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, + sar_band=sar_band, stack_type="tropo", commonMask=common_mask, ) @@ -746,6 +858,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, + sar_band=sar_band, stack_type="set", commonMask=common_mask, ) @@ -756,12 +869,19 @@ def load_nisar(inps): # --------------------------------------------------------------------- # Metadata / subset utilities # --------------------------------------------------------------------- -def extract_metadata(input_files, bbox=None, polarization="HH", frequency="frequencyA"): +def extract_metadata( + input_files, + bbox=None, + polarization="HH", + frequency="frequencyA", + sar_band="LSAR", +): """Extract NISAR metadata for MintPy.""" meta_file = input_files[0] meta = {} - datasets = _datasets_for_pol(polarization, frequency) + datasets = _datasets_for_pol(polarization, frequency, sar_band) + processinfo = _processinfo(sar_band) with h5py.File(meta_file, "r") as ds: pixel_height = ds[datasets["ySpacing"]][()] @@ -771,22 +891,24 @@ def extract_metadata(input_files, bbox=None, polarization="HH", frequency="frequ xcoord = ds[datasets["xcoord"]][()] ycoord = ds[datasets["ycoord"]][()] meta["EPSG"] = int(ds[datasets["epsg"]][()]) - meta["WAVELENGTH"] = SPEED_OF_LIGHT / ds[_center_frequency_path(frequency)][()] - meta["ORBIT_DIRECTION"] = ds[PROCESSINFO["orbit_direction"]][()].decode("utf-8") + meta["WAVELENGTH"] = SPEED_OF_LIGHT / ds[ + _center_frequency_path(frequency, sar_band) + ][()] + meta["ORBIT_DIRECTION"] = ds[processinfo["orbit_direction"]][()].decode("utf-8") meta["POLARIZATION"] = polarization meta["ALOOKS"] = ds[datasets["azimuth_look"]][()] meta["RLOOKS"] = ds[datasets["range_look"]][()] - meta["PLATFORM"] = ds[PROCESSINFO["platform"]][()].decode("utf-8") + meta["PLATFORM"] = ds[processinfo["platform"]][()].decode("utf-8") meta["STARTING_RANGE"] = float( - np.min(ds[PROCESSINFO["rdr_slant_range"]][()].flatten()) + np.min(ds[processinfo["rdr_slant_range"]][()].flatten()) ) start_time = datetime.datetime.strptime( - ds[PROCESSINFO["start_time"]][()].decode("utf-8")[0:26], + ds[processinfo["start_time"]][()].decode("utf-8")[0:26], "%Y-%m-%dT%H:%M:%S.%f", ) end_time = datetime.datetime.strptime( - ds[PROCESSINFO["end_time"]][()].decode("utf-8")[0:26], + ds[processinfo["end_time"]][()].decode("utf-8")[0:26], "%Y-%m-%dT%H:%M:%S.%f", ) @@ -830,7 +952,11 @@ def extract_metadata(input_files, bbox=None, polarization="HH", frequency="frequ utm_bbox = None bounds = common_raster_bound( - input_files, utm_bbox, polarization=polarization, frequency=frequency + input_files, + utm_bbox, + polarization=polarization, + frequency=frequency, + sar_band=sar_band, ) meta["bbox"] = ",".join([str(b) for b in bounds]) @@ -893,21 +1019,24 @@ def get_rows_cols(xcoord, ycoord, bounds): return col1, row1, col2, row2 -def get_raster_corners(input_file, polarization="HH", frequency="frequencyA"): +def get_raster_corners( + input_file, polarization="HH", frequency="frequencyA", sar_band="LSAR" +): """Get the (west, south, east, north) bounds of the image.""" - datasets = _datasets_for_pol(polarization, frequency) + datasets = _datasets_for_pol(polarization, frequency, sar_band) + processinfo = _processinfo(sar_band) with h5py.File(input_file, "r") as ds: xcoord = ds[datasets["xcoord"]][:] ycoord = ds[datasets["ycoord"]][:] - west = max(np.min(ds[PROCESSINFO["rdr_xcoord"]][:]), np.min(xcoord)) - east = min(np.max(ds[PROCESSINFO["rdr_xcoord"]][:]), np.max(xcoord)) - north = min(np.max(ds[PROCESSINFO["rdr_ycoord"]][:]), np.max(ycoord)) - south = max(np.min(ds[PROCESSINFO["rdr_ycoord"]][:]), np.min(ycoord)) + west = max(np.min(ds[processinfo["rdr_xcoord"]][:]), np.min(xcoord)) + east = min(np.max(ds[processinfo["rdr_xcoord"]][:]), np.max(xcoord)) + north = min(np.max(ds[processinfo["rdr_ycoord"]][:]), np.max(ycoord)) + south = max(np.min(ds[processinfo["rdr_ycoord"]][:]), np.min(ycoord)) return float(west), float(south), float(east), float(north) def common_raster_bound( - input_files, utm_bbox=None, polarization="HH", frequency="frequencyA" + input_files, utm_bbox=None, polarization="HH", frequency="frequencyA", sar_band="LSAR" ): """Get common bounds among all data in (xmin, ymin, xmax, ymax).""" wests = [] @@ -916,7 +1045,10 @@ def common_raster_bound( norths = [] for file in input_files: west, south, east, north = get_raster_corners( - file, polarization=polarization, frequency=frequency + file, + polarization=polarization, + frequency=frequency, + sar_band=sar_band, ) wests.append(west) souths.append(south) @@ -963,10 +1095,15 @@ def bbox_to_utm(bbox, dst_epsg, src_epsg=4326): def read_subset( - gunw_file, bbox, polarization="HH", frequency="frequencyA", geometry=False + gunw_file, + bbox, + polarization="HH", + frequency="frequencyA", + sar_band="LSAR", + geometry=False, ): """Read subset arrays or only geometry bounds for unwrapped products.""" - datasets = _datasets_for_pol(polarization, frequency) + datasets = _datasets_for_pol(polarization, frequency, sar_band) with h5py.File(gunw_file, "r") as ds: xcoord = ds[datasets["xcoord"]][()] ycoord = ds[datasets["ycoord"]][()] @@ -1024,6 +1161,7 @@ def read_and_interpolate_geometry( xybbox, polarization="HH", frequency="frequencyA", + sar_band="LSAR", external_mask_file=None, valid_mask=None, ): @@ -1041,6 +1179,7 @@ def read_and_interpolate_geometry( "los_y": "rdr_los_y", }, valid_mask=valid_mask, + sar_band=sar_band, ) slant_range, incidence_angle, azimuth_angle = interpolate_geometry( interp_ctx["x_2d"], @@ -1060,6 +1199,7 @@ def read_and_interpolate_geometry( external_mask_file, polarization, frequency, + sar_band, ) return ( @@ -1096,7 +1236,12 @@ def interpolate_geometry(X_2d, Y_2d, dem, rdr_coords, valid_mask): def read_and_interpolate_troposphere( - gunw_file, dem_file, xybbox, polarization="HH", frequency="frequencyA" + gunw_file, + dem_file, + xybbox, + polarization="HH", + frequency="frequencyA", + sar_band="LSAR", ): """Warp DEM to aligned grid and interpolate combined tropo at valid pixels only.""" interp_ctx = _prepare_radar_grid_interpolation( @@ -1109,6 +1254,7 @@ def read_and_interpolate_troposphere( "wet_tropo": "rdr_wet_tropo", "hydrostatic_tropo": "rdr_hs_tropo", }, + sar_band=sar_band, ) total_tropo = interpolate_troposphere( interp_ctx["x_2d"], @@ -1138,7 +1284,12 @@ def interpolate_troposphere(X_2d, Y_2d, dem, rdr_coords, valid_mask): def read_and_interpolate_SET( - gunw_file, dem_file, xybbox, polarization="HH", frequency="frequencyA" + gunw_file, + dem_file, + xybbox, + polarization="HH", + frequency="frequencyA", + sar_band="LSAR", ): """Warp DEM to aligned grid and interpolate SET phase at valid pixels only.""" interp_ctx = _prepare_radar_grid_interpolation( @@ -1148,6 +1299,7 @@ def read_and_interpolate_SET( polarization, frequency, {"rdr_SET": "rdr_SET"}, + sar_band=sar_band, ) set_phase = interpolate_set( interp_ctx["x_2d"], @@ -1175,17 +1327,18 @@ def interpolate_set(X_2d, Y_2d, dem, rdr_coords, valid_mask): # --------------------------------------------------------------------- # MintPy file builders # --------------------------------------------------------------------- -def _get_date_pairs(filenames): +def _get_date_pairs(filenames, sar_band="LSAR"): """Return reference_secondary date pairs in YYYYMMDD_YYYYMMDD format.""" date12_list = [] + processinfo = _processinfo(sar_band) for filename in filenames: with h5py.File(filename, "r") as ds: if ( - f"{IDENTIFICATION}/referenceZeroDopplerStartTime" in ds - and f"{IDENTIFICATION}/secondaryZeroDopplerStartTime" in ds + processinfo["ref_start_time"] in ds + and processinfo["sec_start_time"] in ds ): - ref_time = ds[f"{IDENTIFICATION}/referenceZeroDopplerStartTime"][()] - sec_time = ds[f"{IDENTIFICATION}/secondaryZeroDopplerStartTime"][()] + ref_time = ds[processinfo["ref_start_time"]][()] + sec_time = ds[processinfo["sec_start_time"]][()] ref_date = ref_time.decode("utf-8").split("T")[0].replace("-", "") sec_date = sec_time.decode("utf-8").split("T")[0].replace("-", "") date12_list.append(f"{ref_date}_{sec_date}") @@ -1214,6 +1367,7 @@ def prepare_geometry( externalMaskFile, polarization="HH", frequency="frequencyA", + sar_band="LSAR", commonMask=None, ): """Prepare the geometry file.""" @@ -1223,7 +1377,12 @@ def prepare_geometry( meta = {key: value for key, value in metadata.items()} geo_ds = read_subset( - metaFile, bbox, polarization=polarization, frequency=frequency, geometry=True + metaFile, + bbox, + polarization=polarization, + frequency=frequency, + sar_band=sar_band, + geometry=True, ) dem_subset_array, slant_range, incidence_angle, azimuth_angle, mask = ( read_and_interpolate_geometry( @@ -1232,6 +1391,7 @@ def prepare_geometry( geo_ds["xybbox"], polarization=polarization, frequency=frequency, + sar_band=sar_band, external_mask_file=externalMaskFile, valid_mask=commonMask, ) @@ -1264,6 +1424,7 @@ def prepare_water_mask( externalMaskFile, polarization="HH", frequency="frequencyA", + sar_band="LSAR", commonMask=None, ): """Prepare a standalone MintPy waterMask.h5 from the GUNW mask.""" @@ -1274,13 +1435,18 @@ def prepare_water_mask( # get subset indices geo_ds = read_subset( - metaFile, bbox, polarization=polarization, frequency=frequency, geometry=True + metaFile, + bbox, + polarization=polarization, + frequency=frequency, + sar_band=sar_band, + geometry=True, ) xybbox = geo_ds["xybbox"] if commonMask is None: water_mask_bool = _read_is_land_and_valid_mask( - metaFile, xybbox, polarization, frequency + metaFile, xybbox, polarization, frequency, sar_band ) else: water_mask_bool = np.asarray(commonMask, dtype=np.bool_) @@ -1299,6 +1465,7 @@ def prepare_water_mask( externalMaskFile, polarization, frequency, + sar_band, ) length, width = water_mask_bool.shape @@ -1321,6 +1488,7 @@ def prepare_stack( date12_list, polarization="HH", frequency="frequencyA", + sar_band="LSAR", stack_type=None, commonMask=None, ): @@ -1334,7 +1502,7 @@ def prepare_stack( print(f"number of inputs/unwrapped interferograms: {num_pair}") missing = _missing_required_paths( - inp_files, effective_stack_type, polarization, frequency + inp_files, effective_stack_type, polarization, frequency, sar_band ) if missing: first_file, first_path = missing[0] @@ -1381,7 +1549,13 @@ def prepare_stack( prog_bar = ptime.progressBar(maxValue=num_pair) for i, file in enumerate(inp_files): obs = _read_stack_observation( - file, effective_stack_type, bbox, demFile, polarization, frequency + file, + effective_stack_type, + bbox, + demFile, + polarization, + frequency, + sar_band, ) obs = _apply_common_mask_to_observation(obs, commonMask) f["unwrapPhase"][i] = obs["unwrap_phase"] From c2793aef631a6e72706fca2d71971f76e1c706f0 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Mon, 18 May 2026 15:28:07 -0700 Subject: [PATCH 4/9] Default NISAR band option to auto --- src/mintpy/cli/prep_nisar.py | 6 +++--- src/mintpy/defaults/smallbaselineApp.cfg | 2 +- src/mintpy/load_data.py | 2 +- src/mintpy/prep_nisar.py | 7 +++++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/mintpy/cli/prep_nisar.py b/src/mintpy/cli/prep_nisar.py index 99c9198d4..8ba04362f 100755 --- a/src/mintpy/cli/prep_nisar.py +++ b/src/mintpy/cli/prep_nisar.py @@ -62,9 +62,9 @@ def create_parser(subparsers=None): "-ba", "--band", dest="sar_band", - choices=["LSAR", "SSAR"], - default="LSAR", - help="NISAR product family to load (default: %(default)s).", + choices=["auto", "LSAR", "SSAR"], + default="auto", + help="NISAR product family to load: auto defaults to LSAR (default: %(default)s).", ) parser.add_argument( diff --git a/src/mintpy/defaults/smallbaselineApp.cfg b/src/mintpy/defaults/smallbaselineApp.cfg index b2d8e9f0c..7e88b3297 100644 --- a/src/mintpy/defaults/smallbaselineApp.cfg +++ b/src/mintpy/defaults/smallbaselineApp.cfg @@ -30,7 +30,7 @@ mintpy.load.processor = auto #[isce, aria, hyp3, gmtsar, snap, gamma, roi mintpy.load.autoPath = auto #[yes / no], auto for no, use pre-defined auto path mintpy.load.updateMode = auto #[yes / no], auto for yes, skip re-loading if HDF5 files are complete mintpy.load.compression = auto #[gzip / lzf / none / default], auto for default (none/lzf for stack/geometry). -mintpy.load.band = LSAR #[LSAR / SSAR], LSAR by default, NISAR only +mintpy.load.band = auto #[auto / LSAR / SSAR], auto for LSAR, NISAR only mintpy.load.frequency = auto #[auto / A / B], auto for A, NISAR only ##---------for ISCE only: mintpy.load.metaFile = auto #[path of common metadata file for the stack], i.e.: ./reference/IW1.xml, ./referenceShelve/data.dat diff --git a/src/mintpy/load_data.py b/src/mintpy/load_data.py index a5156a8d5..5c3223919 100644 --- a/src/mintpy/load_data.py +++ b/src/mintpy/load_data.py @@ -636,7 +636,7 @@ def prepare_metadata(iDict): dem_file = iDict['mintpy.load.demFile'] gunw_files = iDict['mintpy.load.unwFile'] water_mask = iDict['mintpy.load.waterMaskFile'] - band = iDict.get('mintpy.load.band', 'LSAR') + band = iDict.get('mintpy.load.band', 'auto') frequency = iDict.get('mintpy.load.frequency', 'auto') if str(dem_file).lower() in ['auto', 'none', 'no', '']: diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index e9219347d..38708cb86 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -49,9 +49,12 @@ def _normalize_frequency(frequency) -> str: def _normalize_sar_band(sar_band) -> str: """Return the normalized NISAR product family name.""" - normalized = "LSAR" if sar_band is None else str(sar_band).upper() + if sar_band is None or str(sar_band).lower() == "auto": + return "LSAR" + + normalized = str(sar_band).upper() if normalized not in {"LSAR", "SSAR"}: - raise ValueError("sar_band must be one of: LSAR, SSAR") + raise ValueError("sar_band must be one of: auto, LSAR, SSAR") return normalized From 0dc01a1ab1cd0910d5f2f7fca7b7474979701533 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 27 May 2026 12:56:42 -0700 Subject: [PATCH 5/9] Normalize NISAR SAR band handling and centralize LSAR default --- src/mintpy/prep_nisar.py | 109 +++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index 38708cb86..9ff055768 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -29,6 +29,8 @@ "frequencyA": "frequencyA", "frequencyB": "frequencyB", } +DEFAULT_SAR_BAND = "LSAR" +VALID_SAR_BANDS = {DEFAULT_SAR_BAND, "SSAR"} STACK_TYPES = {"ifgram", "ion", "tropo", "set"} @@ -50,27 +52,29 @@ def _normalize_frequency(frequency) -> str: def _normalize_sar_band(sar_band) -> str: """Return the normalized NISAR product family name.""" if sar_band is None or str(sar_band).lower() == "auto": - return "LSAR" + return DEFAULT_SAR_BAND normalized = str(sar_band).upper() - if normalized not in {"LSAR", "SSAR"}: - raise ValueError("sar_band must be one of: auto, LSAR, SSAR") + if normalized not in VALID_SAR_BANDS: + raise ValueError( + f"sar_band must be one of: auto, {', '.join(sorted(VALID_SAR_BANDS))}" + ) return normalized -def _science_root(sar_band: str = "LSAR") -> str: - return f"/science/{_normalize_sar_band(sar_band)}" +def _science_root(sar_band: str = DEFAULT_SAR_BAND) -> str: + return f"/science/{sar_band}" -def _identification_root(sar_band: str = "LSAR") -> str: +def _identification_root(sar_band: str = DEFAULT_SAR_BAND) -> str: return f"{_science_root(sar_band)}/identification" -def _radargrid_root(sar_band: str = "LSAR") -> str: +def _radargrid_root(sar_band: str = DEFAULT_SAR_BAND) -> str: return f"{_science_root(sar_band)}/GUNW/metadata/radarGrid" -def _processinfo(sar_band: str = "LSAR") -> dict: +def _processinfo(sar_band: str = DEFAULT_SAR_BAND) -> dict: identification = _identification_root(sar_band) radargrid_root = _radargrid_root(sar_band) return { @@ -94,23 +98,25 @@ def _processinfo(sar_band: str = "LSAR") -> dict: } -def _dataset_root_unw(frequency: str, sar_band: str = "LSAR") -> str: +def _dataset_root_unw(frequency: str, sar_band: str = DEFAULT_SAR_BAND) -> str: return f"{_science_root(sar_band)}/GUNW/grids/{frequency}/unwrappedInterferogram" -def _parameters_root(frequency: str, sar_band: str = "LSAR") -> str: +def _parameters_root(frequency: str, sar_band: str = DEFAULT_SAR_BAND) -> str: return ( f"{_science_root(sar_band)}/GUNW/metadata/processingInformation/parameters/" f"unwrappedInterferogram/{frequency}" ) -def _center_frequency_path(frequency: str, sar_band: str = "LSAR") -> str: +def _center_frequency_path( + frequency: str, sar_band: str = DEFAULT_SAR_BAND +) -> str: return f"{_science_root(sar_band)}/GUNW/grids/{frequency}/centerFrequency" def _datasets_for_pol( - polarization: str, frequency: str, sar_band: str = "LSAR" + polarization: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND ) -> dict: """Return per-call dataset paths for the selected frequency/polarization.""" root = _dataset_root_unw(frequency, sar_band) @@ -133,7 +139,10 @@ def _datasets_for_pol( def _resolve_frequency( - gunw_file: str, frequency, polarization: str, sar_band: str = "LSAR" + gunw_file: str, + frequency, + polarization: str, + sar_band: str = DEFAULT_SAR_BAND, ) -> str: """Resolve and validate the requested NISAR frequency.""" resolved = _normalize_frequency(frequency) @@ -161,7 +170,7 @@ def _resolve_frequency( "Check that the input file contains frequencyB for this polarization." ) raise ValueError( - f"NISAR {_normalize_sar_band(sar_band)} {requested} data for " + f"NISAR {sar_band} {requested} data for " f"polarization {polarization!r} was not found in {gunw_file}. " f"Missing path: {missing[0]}. {hint}" ) @@ -293,7 +302,7 @@ def _read_unwrapped_phase_valid_mask( xybbox, pol: str, frequency: str, - sar_band: str = "LSAR", + sar_band: str = DEFAULT_SAR_BAND, ): """Fallback validity mask based on finite unwrappedPhase (+ _FillValue check).""" datasets = _datasets_for_pol(pol, frequency, sar_band) @@ -314,7 +323,7 @@ def _read_is_land_and_valid_mask( xybbox, pol: str, frequency: str, - sar_band: str = "LSAR", + sar_band: str = DEFAULT_SAR_BAND, ): """Decode the native GUNW mask into MintPy's keep-mask convention.""" datasets = _datasets_for_pol(pol, frequency, sar_band) @@ -344,7 +353,11 @@ def _read_is_land_and_valid_mask( def _read_common_is_land_and_valid_mask( - input_files, bbox, pol: str, frequency: str, sar_band: str = "LSAR" + input_files, + bbox, + pol: str, + frequency: str, + sar_band: str = DEFAULT_SAR_BAND, ): """Return the common keep-mask across all input GUNW products.""" common_mask = None @@ -389,7 +402,7 @@ def _apply_external_mask( external_mask_file, polarization: str, frequency: str, - sar_band: str = "LSAR", + sar_band: str = DEFAULT_SAR_BAND, ): """Refine a keep-mask with an optional external raster mask.""" if not _external_mask_is_set(external_mask_file): @@ -411,7 +424,7 @@ def _apply_external_mask( def _read_perpendicular_baseline( - gunw_file: str, sar_band: str = "LSAR" + gunw_file: str, sar_band: str = DEFAULT_SAR_BAND ) -> np.float32: """Read the NISAR perpendicular baseline as one finite mean value.""" processinfo = _processinfo(sar_band) @@ -438,7 +451,7 @@ def _read_target_grid( xybbox, polarization: str, frequency: str, - sar_band: str = "LSAR", + sar_band: str = DEFAULT_SAR_BAND, ): """Read the destination EPSG and subset grid axes from a GUNW file.""" datasets = _datasets_for_pol(polarization, frequency, sar_band) @@ -451,7 +464,9 @@ def _read_target_grid( def _read_radar_grid_fields( - gunw_file: str, field_map: dict, sar_band: str = "LSAR" + gunw_file: str, + field_map: dict, + sar_band: str = DEFAULT_SAR_BAND, ): """Read radar-grid interpolation axes plus the requested data fields.""" processinfo = _processinfo(sar_band) @@ -473,7 +488,7 @@ def _prepare_radar_grid_interpolation( frequency, field_map, valid_mask=None, - sar_band: str = "LSAR", + sar_band: str = DEFAULT_SAR_BAND, ): """Build the common DEM/grid/valid-mask context for radar-grid interpolation.""" dem_src_epsg = _read_raster_epsg(dem_file) @@ -575,7 +590,10 @@ def _resolve_stack_type(stack_type, outfile): def _required_paths_for_stack_type( - stack_type, polarization, frequency, sar_band: str = "LSAR" + stack_type, + polarization, + frequency, + sar_band: str = DEFAULT_SAR_BAND, ): """Return HDF5 source datasets needed to build the requested stack.""" datasets = _datasets_for_pol(polarization, frequency, sar_band) @@ -595,7 +613,11 @@ def _required_paths_for_stack_type( def _missing_required_paths( - inp_files, stack_type, polarization, frequency, sar_band: str = "LSAR" + inp_files, + stack_type, + polarization, + frequency, + sar_band: str = DEFAULT_SAR_BAND, ): """Return missing required HDF5 source paths as (file, path) pairs.""" required_paths = _required_paths_for_stack_type( @@ -617,7 +639,7 @@ def _read_stack_observation( dem_file, polarization, frequency, - sar_band: str = "LSAR", + sar_band: str = DEFAULT_SAR_BAND, ): """Read one observation for the requested stack type.""" pbase = _read_perpendicular_baseline(file, sar_band) @@ -731,11 +753,15 @@ def load_nisar(inps): # extract metadata pol = getattr(inps, "polarization", "HH") - sar_band = getattr(inps, "sar_band", "LSAR") + # Normalize once here so downstream helpers can assume a concrete product family. + sar_band = _normalize_sar_band( + getattr(inps, "sar_band", DEFAULT_SAR_BAND) + ) + inps.sar_band = sar_band frequency = _resolve_frequency( input_files[0], getattr(inps, "frequency", "auto"), pol, sar_band ) - print(f"Using NISAR {_normalize_sar_band(sar_band)} {frequency}") + print(f"Using NISAR {sar_band} {frequency}") metadata, bounds = extract_metadata( input_files, bbox=bbox, @@ -877,7 +903,7 @@ def extract_metadata( bbox=None, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, ): """Extract NISAR metadata for MintPy.""" meta_file = input_files[0] @@ -1023,7 +1049,10 @@ def get_rows_cols(xcoord, ycoord, bounds): def get_raster_corners( - input_file, polarization="HH", frequency="frequencyA", sar_band="LSAR" + input_file, + polarization="HH", + frequency="frequencyA", + sar_band=DEFAULT_SAR_BAND, ): """Get the (west, south, east, north) bounds of the image.""" datasets = _datasets_for_pol(polarization, frequency, sar_band) @@ -1039,7 +1068,11 @@ def get_raster_corners( def common_raster_bound( - input_files, utm_bbox=None, polarization="HH", frequency="frequencyA", sar_band="LSAR" + input_files, + utm_bbox=None, + polarization="HH", + frequency="frequencyA", + sar_band=DEFAULT_SAR_BAND, ): """Get common bounds among all data in (xmin, ymin, xmax, ymax).""" wests = [] @@ -1102,7 +1135,7 @@ def read_subset( bbox, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, geometry=False, ): """Read subset arrays or only geometry bounds for unwrapped products.""" @@ -1164,7 +1197,7 @@ def read_and_interpolate_geometry( xybbox, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, external_mask_file=None, valid_mask=None, ): @@ -1244,7 +1277,7 @@ def read_and_interpolate_troposphere( xybbox, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, ): """Warp DEM to aligned grid and interpolate combined tropo at valid pixels only.""" interp_ctx = _prepare_radar_grid_interpolation( @@ -1292,7 +1325,7 @@ def read_and_interpolate_SET( xybbox, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, ): """Warp DEM to aligned grid and interpolate SET phase at valid pixels only.""" interp_ctx = _prepare_radar_grid_interpolation( @@ -1330,7 +1363,7 @@ def interpolate_set(X_2d, Y_2d, dem, rdr_coords, valid_mask): # --------------------------------------------------------------------- # MintPy file builders # --------------------------------------------------------------------- -def _get_date_pairs(filenames, sar_band="LSAR"): +def _get_date_pairs(filenames, sar_band=DEFAULT_SAR_BAND): """Return reference_secondary date pairs in YYYYMMDD_YYYYMMDD format.""" date12_list = [] processinfo = _processinfo(sar_band) @@ -1370,7 +1403,7 @@ def prepare_geometry( externalMaskFile, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, commonMask=None, ): """Prepare the geometry file.""" @@ -1427,7 +1460,7 @@ def prepare_water_mask( externalMaskFile, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, commonMask=None, ): """Prepare a standalone MintPy waterMask.h5 from the GUNW mask.""" @@ -1491,7 +1524,7 @@ def prepare_stack( date12_list, polarization="HH", frequency="frequencyA", - sar_band="LSAR", + sar_band=DEFAULT_SAR_BAND, stack_type=None, commonMask=None, ): From 9679750b1e603b288581f47ffb911d82b1e4f71a Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 27 May 2026 13:14:04 -0700 Subject: [PATCH 6/9] Refactor prep_nisar helpers around a shared NISAR product context --- src/mintpy/prep_nisar.py | 412 ++++++++++++++++++++++++++++----------- 1 file changed, 293 insertions(+), 119 deletions(-) diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index 9ff055768..a543672c2 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -9,7 +9,9 @@ import datetime import glob import os +from dataclasses import dataclass, field from pathlib import Path +from typing import Dict, Optional import h5py import numpy as np @@ -62,80 +64,155 @@ def _normalize_sar_band(sar_band) -> str: return normalized -def _science_root(sar_band: str = DEFAULT_SAR_BAND) -> str: - return f"/science/{sar_band}" +@dataclass(frozen=True) +class NisarProductContext: + """Cache the band-specific HDF5 roots and commonly reused dataset paths.""" + + sar_band: str = DEFAULT_SAR_BAND + science_root: str = field(init=False) + identification_root: str = field(init=False) + radargrid_root: str = field(init=False) + processinfo: Dict[str, str] = field(init=False) + + def __post_init__(self): + sar_band = _normalize_sar_band(self.sar_band) + science_root = f"/science/{sar_band}" + identification_root = f"{science_root}/identification" + radargrid_root = f"{science_root}/GUNW/metadata/radarGrid" + + object.__setattr__(self, "sar_band", sar_band) + object.__setattr__(self, "science_root", science_root) + object.__setattr__(self, "identification_root", identification_root) + object.__setattr__(self, "radargrid_root", radargrid_root) + object.__setattr__( + self, + "processinfo", + { + "orbit_direction": f"{identification_root}/orbitPassDirection", + "platform": f"{identification_root}/missionId", + "start_time": f"{identification_root}/referenceZeroDopplerStartTime", + "end_time": f"{identification_root}/referenceZeroDopplerEndTime", + "ref_start_time": f"{identification_root}/referenceZeroDopplerStartTime", + "sec_start_time": f"{identification_root}/secondaryZeroDopplerStartTime", + "rdr_xcoord": f"{radargrid_root}/xCoordinates", + "rdr_ycoord": f"{radargrid_root}/yCoordinates", + "rdr_slant_range": f"{radargrid_root}/referenceSlantRange", + "rdr_height": f"{radargrid_root}/heightAboveEllipsoid", + "rdr_incidence": f"{radargrid_root}/incidenceAngle", + "rdr_los_x": f"{radargrid_root}/losUnitVectorX", + "rdr_los_y": f"{radargrid_root}/losUnitVectorY", + "rdr_wet_tropo": f"{radargrid_root}/wetTroposphericPhaseScreen", + "rdr_hs_tropo": f"{radargrid_root}/hydrostaticTroposphericPhaseScreen", + "rdr_SET": f"{radargrid_root}/slantRangeSolidEarthTidesPhase", + "bperp": f"{radargrid_root}/perpendicularBaseline", + }, + ) + + def dataset_root_unw(self, frequency: str) -> str: + return f"{self.science_root}/GUNW/grids/{frequency}/unwrappedInterferogram" + + def parameters_root(self, frequency: str) -> str: + return ( + f"{self.science_root}/GUNW/metadata/processingInformation/parameters/" + f"unwrappedInterferogram/{frequency}" + ) + def center_frequency_path(self, frequency: str) -> str: + return f"{self.science_root}/GUNW/grids/{frequency}/centerFrequency" -def _identification_root(sar_band: str = DEFAULT_SAR_BAND) -> str: - return f"{_science_root(sar_band)}/identification" + def datasets_for_pol(self, polarization: str, frequency: str) -> Dict[str, str]: + root = self.dataset_root_unw(frequency) + parameters = self.parameters_root(frequency) + return { + "xcoord": f"{root}/{polarization}/xCoordinates", + "ycoord": f"{root}/{polarization}/yCoordinates", + "unw": f"{root}/{polarization}/unwrappedPhase", + "mask": f"{root}/mask", + "cor": f"{root}/{polarization}/coherenceMagnitude", + "connComp": f"{root}/{polarization}/connectedComponents", + "ion": f"{root}/{polarization}/ionospherePhaseScreen", + "epsg": f"{root}/{polarization}/projection", + "xSpacing": f"{root}/{polarization}/xCoordinateSpacing", + "ySpacing": f"{root}/{polarization}/yCoordinateSpacing", + "polarization": ( + f"{self.science_root}/GUNW/grids/{frequency}/listOfPolarizations" + ), + "range_look": f"{parameters}/numberOfRangeLooks", + "azimuth_look": f"{parameters}/numberOfAzimuthLooks", + } -def _radargrid_root(sar_band: str = DEFAULT_SAR_BAND) -> str: - return f"{_science_root(sar_band)}/GUNW/metadata/radarGrid" +def _coerce_product_context( + product_ctx: Optional[NisarProductContext] = None, + sar_band: str = DEFAULT_SAR_BAND, +) -> NisarProductContext: + return product_ctx if product_ctx is not None else NisarProductContext(sar_band) -def _processinfo(sar_band: str = DEFAULT_SAR_BAND) -> dict: - identification = _identification_root(sar_band) - radargrid_root = _radargrid_root(sar_band) - return { - "orbit_direction": f"{identification}/orbitPassDirection", - "platform": f"{identification}/missionId", - "start_time": f"{identification}/referenceZeroDopplerStartTime", - "end_time": f"{identification}/referenceZeroDopplerEndTime", - "ref_start_time": f"{identification}/referenceZeroDopplerStartTime", - "sec_start_time": f"{identification}/secondaryZeroDopplerStartTime", - "rdr_xcoord": f"{radargrid_root}/xCoordinates", - "rdr_ycoord": f"{radargrid_root}/yCoordinates", - "rdr_slant_range": f"{radargrid_root}/referenceSlantRange", - "rdr_height": f"{radargrid_root}/heightAboveEllipsoid", - "rdr_incidence": f"{radargrid_root}/incidenceAngle", - "rdr_los_x": f"{radargrid_root}/losUnitVectorX", - "rdr_los_y": f"{radargrid_root}/losUnitVectorY", - "rdr_wet_tropo": f"{radargrid_root}/wetTroposphericPhaseScreen", - "rdr_hs_tropo": f"{radargrid_root}/hydrostaticTroposphericPhaseScreen", - "rdr_SET": f"{radargrid_root}/slantRangeSolidEarthTidesPhase", - "bperp": f"{radargrid_root}/perpendicularBaseline", - } +def _science_root( + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> str: + return _coerce_product_context(product_ctx, sar_band).science_root -def _dataset_root_unw(frequency: str, sar_band: str = DEFAULT_SAR_BAND) -> str: - return f"{_science_root(sar_band)}/GUNW/grids/{frequency}/unwrappedInterferogram" +def _identification_root( + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> str: + return _coerce_product_context(product_ctx, sar_band).identification_root -def _parameters_root(frequency: str, sar_band: str = DEFAULT_SAR_BAND) -> str: - return ( - f"{_science_root(sar_band)}/GUNW/metadata/processingInformation/parameters/" - f"unwrappedInterferogram/{frequency}" - ) +def _radargrid_root( + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> str: + return _coerce_product_context(product_ctx, sar_band).radargrid_root + + +def _processinfo( + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> Dict[str, str]: + return _coerce_product_context(product_ctx, sar_band).processinfo + + +def _dataset_root_unw( + frequency: str, + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> str: + return _coerce_product_context(product_ctx, sar_band).dataset_root_unw(frequency) + + +def _parameters_root( + frequency: str, + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> str: + return _coerce_product_context(product_ctx, sar_band).parameters_root(frequency) def _center_frequency_path( - frequency: str, sar_band: str = DEFAULT_SAR_BAND + frequency: str, + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ) -> str: - return f"{_science_root(sar_band)}/GUNW/grids/{frequency}/centerFrequency" + return _coerce_product_context(product_ctx, sar_band).center_frequency_path( + frequency + ) def _datasets_for_pol( - polarization: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND -) -> dict: + polarization: str, + frequency: str, + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +) -> Dict[str, str]: """Return per-call dataset paths for the selected frequency/polarization.""" - root = _dataset_root_unw(frequency, sar_band) - parameters = _parameters_root(frequency, sar_band) - return { - "xcoord": f"{root}/{polarization}/xCoordinates", - "ycoord": f"{root}/{polarization}/yCoordinates", - "unw": f"{root}/{polarization}/unwrappedPhase", - "mask": f"{root}/mask", - "cor": f"{root}/{polarization}/coherenceMagnitude", - "connComp": f"{root}/{polarization}/connectedComponents", - "ion": f"{root}/{polarization}/ionospherePhaseScreen", - "epsg": f"{root}/{polarization}/projection", - "xSpacing": f"{root}/{polarization}/xCoordinateSpacing", - "ySpacing": f"{root}/{polarization}/yCoordinateSpacing", - "polarization": f"{_science_root(sar_band)}/GUNW/grids/{frequency}/listOfPolarizations", - "range_look": f"{parameters}/numberOfRangeLooks", - "azimuth_look": f"{parameters}/numberOfAzimuthLooks", - } + return _coerce_product_context(product_ctx, sar_band).datasets_for_pol( + polarization, frequency + ) def _resolve_frequency( @@ -143,16 +220,20 @@ def _resolve_frequency( frequency, polarization: str, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ) -> str: """Resolve and validate the requested NISAR frequency.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) resolved = _normalize_frequency(frequency) - datasets = _datasets_for_pol(polarization, resolved, sar_band) + datasets = _datasets_for_pol( + polarization, resolved, product_ctx=product_ctx + ) with h5py.File(gunw_file, "r") as ds: required_paths = [ - _dataset_root_unw(resolved, sar_band), + _dataset_root_unw(resolved, product_ctx=product_ctx), datasets["unw"], - _center_frequency_path(resolved, sar_band), + _center_frequency_path(resolved, product_ctx=product_ctx), ] missing = [path for path in required_paths if path not in ds] @@ -170,7 +251,7 @@ def _resolve_frequency( "Check that the input file contains frequencyB for this polarization." ) raise ValueError( - f"NISAR {sar_band} {requested} data for " + f"NISAR {product_ctx.sar_band} {requested} data for " f"polarization {polarization!r} was not found in {gunw_file}. " f"Missing path: {missing[0]}. {hint}" ) @@ -303,9 +384,11 @@ def _read_unwrapped_phase_valid_mask( pol: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Fallback validity mask based on finite unwrappedPhase (+ _FillValue check).""" - datasets = _datasets_for_pol(pol, frequency, sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol(pol, frequency, product_ctx=product_ctx) path = datasets["unw"] with h5py.File(gunw_file, "r") as ds: dset = ds[path] @@ -324,15 +407,21 @@ def _read_is_land_and_valid_mask( pol: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Decode the native GUNW mask into MintPy's keep-mask convention.""" - datasets = _datasets_for_pol(pol, frequency, sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol(pol, frequency, product_ctx=product_ctx) path = datasets["mask"] with h5py.File(gunw_file, "r") as ds: if path not in ds: return _read_unwrapped_phase_valid_mask( - gunw_file, xybbox, pol, frequency, sar_band + gunw_file, + xybbox, + pol, + frequency, + product_ctx=product_ctx, ) dset = ds[path] @@ -358,8 +447,10 @@ def _read_common_is_land_and_valid_mask( pol: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Return the common keep-mask across all input GUNW products.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) common_mask = None for gunw_file in input_files: geo_ds = read_subset( @@ -367,11 +458,15 @@ def _read_common_is_land_and_valid_mask( bbox, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, geometry=True, ) mask = _read_is_land_and_valid_mask( - gunw_file, geo_ds["xybbox"], pol, frequency, sar_band + gunw_file, + geo_ds["xybbox"], + pol, + frequency, + product_ctx=product_ctx, ).astype(bool, copy=False) if common_mask is None: @@ -403,13 +498,19 @@ def _apply_external_mask( polarization: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Refine a keep-mask with an optional external raster mask.""" if not _external_mask_is_set(external_mask_file): return keep_mask + product_ctx = _coerce_product_context(product_ctx, sar_band) dst_epsg, xcoord, ycoord = _read_target_grid( - gunw_file, xybbox, polarization, frequency, sar_band + gunw_file, + xybbox, + polarization, + frequency, + product_ctx=product_ctx, ) mask_src_epsg = _read_raster_epsg(external_mask_file) external_mask = _warp_to_grid_mem( @@ -424,10 +525,13 @@ def _apply_external_mask( def _read_perpendicular_baseline( - gunw_file: str, sar_band: str = DEFAULT_SAR_BAND + gunw_file: str, + sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ) -> np.float32: """Read the NISAR perpendicular baseline as one finite mean value.""" - processinfo = _processinfo(sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + processinfo = _processinfo(product_ctx=product_ctx) with h5py.File(gunw_file, "r") as ds: dset = ds[processinfo["bperp"]] bperp = np.asarray(dset[()], dtype=np.float64).reshape(-1) @@ -452,9 +556,13 @@ def _read_target_grid( polarization: str, frequency: str, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Read the destination EPSG and subset grid axes from a GUNW file.""" - datasets = _datasets_for_pol(polarization, frequency, sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol( + polarization, frequency, product_ctx=product_ctx + ) with h5py.File(gunw_file, "r") as ds: return ( int(ds[datasets["epsg"]][()]), @@ -467,9 +575,11 @@ def _read_radar_grid_fields( gunw_file: str, field_map: dict, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Read radar-grid interpolation axes plus the requested data fields.""" - processinfo = _processinfo(sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + processinfo = _processinfo(product_ctx=product_ctx) rdr_coords = {} with h5py.File(gunw_file, "r") as ds: rdr_coords["xcoord_radar_grid"] = ds[processinfo["rdr_xcoord"]][()] @@ -489,13 +599,21 @@ def _prepare_radar_grid_interpolation( field_map, valid_mask=None, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Build the common DEM/grid/valid-mask context for radar-grid interpolation.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) dem_src_epsg = _read_raster_epsg(dem_file) dst_epsg, xcoord, ycoord = _read_target_grid( - gunw_file, xybbox, polarization, frequency, sar_band + gunw_file, + xybbox, + polarization, + frequency, + product_ctx=product_ctx, + ) + rdr_coords = _read_radar_grid_fields( + gunw_file, field_map, product_ctx=product_ctx ) - rdr_coords = _read_radar_grid_fields(gunw_file, field_map, sar_band) dem_subset_array = _warp_to_grid_mem( src_path=dem_file, @@ -509,7 +627,11 @@ def _prepare_radar_grid_interpolation( y_2d, x_2d = np.meshgrid(ycoord, xcoord, indexing="ij") if valid_mask is None: valid_mask = _read_is_land_and_valid_mask( - gunw_file, xybbox, polarization, frequency, sar_band + gunw_file, + xybbox, + polarization, + frequency, + product_ctx=product_ctx, ) else: valid_mask = np.asarray(valid_mask, dtype=np.bool_) @@ -594,10 +716,14 @@ def _required_paths_for_stack_type( polarization, frequency, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Return HDF5 source datasets needed to build the requested stack.""" - datasets = _datasets_for_pol(polarization, frequency, sar_band) - processinfo = _processinfo(sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol( + polarization, frequency, product_ctx=product_ctx + ) + processinfo = _processinfo(product_ctx=product_ctx) if stack_type == "ifgram": return [datasets["unw"], datasets["cor"], datasets["connComp"]] if stack_type == "ion": @@ -618,10 +744,12 @@ def _missing_required_paths( polarization, frequency, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Return missing required HDF5 source paths as (file, path) pairs.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) required_paths = _required_paths_for_stack_type( - stack_type, polarization, frequency, sar_band + stack_type, polarization, frequency, product_ctx=product_ctx ) missing = [] @@ -640,9 +768,11 @@ def _read_stack_observation( polarization, frequency, sar_band: str = DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Read one observation for the requested stack type.""" - pbase = _read_perpendicular_baseline(file, sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + pbase = _read_perpendicular_baseline(file, product_ctx=product_ctx) if stack_type in {"ifgram", "ion"}: dataset = read_subset( @@ -650,7 +780,7 @@ def _read_stack_observation( bbox, polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) unwrap_key = "unw_data" if stack_type == "ifgram" else "ion_data" return { @@ -665,7 +795,7 @@ def _read_stack_observation( bbox, polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, geometry=True, ) if stack_type == "tropo": @@ -675,7 +805,7 @@ def _read_stack_observation( geo_ds["xybbox"], polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) else: unwrap_phase = read_and_interpolate_SET( @@ -684,7 +814,7 @@ def _read_stack_observation( geo_ds["xybbox"], polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) return {"unwrap_phase": unwrap_phase, "pbase": pbase} @@ -754,30 +884,35 @@ def load_nisar(inps): # extract metadata pol = getattr(inps, "polarization", "HH") # Normalize once here so downstream helpers can assume a concrete product family. - sar_band = _normalize_sar_band( - getattr(inps, "sar_band", DEFAULT_SAR_BAND) - ) - inps.sar_band = sar_band + product_ctx = NisarProductContext(getattr(inps, "sar_band", DEFAULT_SAR_BAND)) + inps.sar_band = product_ctx.sar_band frequency = _resolve_frequency( - input_files[0], getattr(inps, "frequency", "auto"), pol, sar_band + input_files[0], + getattr(inps, "frequency", "auto"), + pol, + product_ctx=product_ctx, ) - print(f"Using NISAR {sar_band} {frequency}") + print(f"Using NISAR {product_ctx.sar_band} {frequency}") metadata, bounds = extract_metadata( input_files, bbox=bbox, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) common_mask = _read_common_is_land_and_valid_mask( - input_files, bounds, pol=pol, frequency=frequency, sar_band=sar_band + input_files, + bounds, + pol=pol, + frequency=frequency, + product_ctx=product_ctx, ) first_geo_ds = read_subset( input_files[0], bounds, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, geometry=True, ) common_mask = _apply_external_mask( @@ -787,7 +922,7 @@ def load_nisar(inps): inps.mask_file, pol, frequency, - sar_band, + product_ctx=product_ctx, ) print( "Common valid land pixels from all NISAR masks: " @@ -802,7 +937,7 @@ def load_nisar(inps): set_stack_file = os.path.join(inps.out_dir, "inputs/setStack.h5") # date pairs - date12_list = _get_date_pairs(input_files, sar_band=sar_band) + date12_list = _get_date_pairs(input_files, product_ctx=product_ctx) # geometry metadata = prepare_geometry( @@ -815,7 +950,7 @@ def load_nisar(inps): commonMask=common_mask, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) # standalone water mask (MintPy format) @@ -829,7 +964,7 @@ def load_nisar(inps): commonMask=common_mask, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) # ifgram stack @@ -842,7 +977,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, stack_type="ifgram", commonMask=common_mask, ) @@ -857,7 +992,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, stack_type="ion", commonMask=common_mask, ) @@ -872,7 +1007,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, stack_type="tropo", commonMask=common_mask, ) @@ -887,7 +1022,7 @@ def load_nisar(inps): date12_list=date12_list, polarization=pol, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, stack_type="set", commonMask=common_mask, ) @@ -904,13 +1039,17 @@ def extract_metadata( polarization="HH", frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Extract NISAR metadata for MintPy.""" meta_file = input_files[0] meta = {} - datasets = _datasets_for_pol(polarization, frequency, sar_band) - processinfo = _processinfo(sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol( + polarization, frequency, product_ctx=product_ctx + ) + processinfo = _processinfo(product_ctx=product_ctx) with h5py.File(meta_file, "r") as ds: pixel_height = ds[datasets["ySpacing"]][()] @@ -921,7 +1060,7 @@ def extract_metadata( ycoord = ds[datasets["ycoord"]][()] meta["EPSG"] = int(ds[datasets["epsg"]][()]) meta["WAVELENGTH"] = SPEED_OF_LIGHT / ds[ - _center_frequency_path(frequency, sar_band) + _center_frequency_path(frequency, product_ctx=product_ctx) ][()] meta["ORBIT_DIRECTION"] = ds[processinfo["orbit_direction"]][()].decode("utf-8") meta["POLARIZATION"] = polarization @@ -985,7 +1124,7 @@ def extract_metadata( utm_bbox, polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) meta["bbox"] = ",".join([str(b) for b in bounds]) @@ -1053,10 +1192,14 @@ def get_raster_corners( polarization="HH", frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Get the (west, south, east, north) bounds of the image.""" - datasets = _datasets_for_pol(polarization, frequency, sar_band) - processinfo = _processinfo(sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol( + polarization, frequency, product_ctx=product_ctx + ) + processinfo = _processinfo(product_ctx=product_ctx) with h5py.File(input_file, "r") as ds: xcoord = ds[datasets["xcoord"]][:] ycoord = ds[datasets["ycoord"]][:] @@ -1073,8 +1216,10 @@ def common_raster_bound( polarization="HH", frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Get common bounds among all data in (xmin, ymin, xmax, ymax).""" + product_ctx = _coerce_product_context(product_ctx, sar_band) wests = [] souths = [] easts = [] @@ -1084,7 +1229,7 @@ def common_raster_bound( file, polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, ) wests.append(west) souths.append(south) @@ -1137,9 +1282,13 @@ def read_subset( frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, geometry=False, + product_ctx: Optional[NisarProductContext] = None, ): """Read subset arrays or only geometry bounds for unwrapped products.""" - datasets = _datasets_for_pol(polarization, frequency, sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + datasets = _datasets_for_pol( + polarization, frequency, product_ctx=product_ctx + ) with h5py.File(gunw_file, "r") as ds: xcoord = ds[datasets["xcoord"]][()] ycoord = ds[datasets["ycoord"]][()] @@ -1200,8 +1349,10 @@ def read_and_interpolate_geometry( sar_band=DEFAULT_SAR_BAND, external_mask_file=None, valid_mask=None, + product_ctx: Optional[NisarProductContext] = None, ): """Warp DEM to the interferogram grid and interpolate geometry layers.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) interp_ctx = _prepare_radar_grid_interpolation( gunw_file, dem_file, @@ -1215,7 +1366,7 @@ def read_and_interpolate_geometry( "los_y": "rdr_los_y", }, valid_mask=valid_mask, - sar_band=sar_band, + product_ctx=product_ctx, ) slant_range, incidence_angle, azimuth_angle = interpolate_geometry( interp_ctx["x_2d"], @@ -1235,7 +1386,7 @@ def read_and_interpolate_geometry( external_mask_file, polarization, frequency, - sar_band, + product_ctx=product_ctx, ) return ( @@ -1278,8 +1429,10 @@ def read_and_interpolate_troposphere( polarization="HH", frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Warp DEM to aligned grid and interpolate combined tropo at valid pixels only.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) interp_ctx = _prepare_radar_grid_interpolation( gunw_file, dem_file, @@ -1290,7 +1443,7 @@ def read_and_interpolate_troposphere( "wet_tropo": "rdr_wet_tropo", "hydrostatic_tropo": "rdr_hs_tropo", }, - sar_band=sar_band, + product_ctx=product_ctx, ) total_tropo = interpolate_troposphere( interp_ctx["x_2d"], @@ -1326,8 +1479,10 @@ def read_and_interpolate_SET( polarization="HH", frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, ): """Warp DEM to aligned grid and interpolate SET phase at valid pixels only.""" + product_ctx = _coerce_product_context(product_ctx, sar_band) interp_ctx = _prepare_radar_grid_interpolation( gunw_file, dem_file, @@ -1335,7 +1490,7 @@ def read_and_interpolate_SET( polarization, frequency, {"rdr_SET": "rdr_SET"}, - sar_band=sar_band, + product_ctx=product_ctx, ) set_phase = interpolate_set( interp_ctx["x_2d"], @@ -1363,10 +1518,15 @@ def interpolate_set(X_2d, Y_2d, dem, rdr_coords, valid_mask): # --------------------------------------------------------------------- # MintPy file builders # --------------------------------------------------------------------- -def _get_date_pairs(filenames, sar_band=DEFAULT_SAR_BAND): +def _get_date_pairs( + filenames, + sar_band=DEFAULT_SAR_BAND, + product_ctx: Optional[NisarProductContext] = None, +): """Return reference_secondary date pairs in YYYYMMDD_YYYYMMDD format.""" date12_list = [] - processinfo = _processinfo(sar_band) + product_ctx = _coerce_product_context(product_ctx, sar_band) + processinfo = _processinfo(product_ctx=product_ctx) for filename in filenames: with h5py.File(filename, "r") as ds: if ( @@ -1405,19 +1565,21 @@ def prepare_geometry( frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, commonMask=None, + product_ctx: Optional[NisarProductContext] = None, ): """Prepare the geometry file.""" print("-" * 50) print(f"preparing geometry file: {outfile}") meta = {key: value for key, value in metadata.items()} + product_ctx = _coerce_product_context(product_ctx, sar_band) geo_ds = read_subset( metaFile, bbox, polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, geometry=True, ) dem_subset_array, slant_range, incidence_angle, azimuth_angle, mask = ( @@ -1427,7 +1589,7 @@ def prepare_geometry( geo_ds["xybbox"], polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, external_mask_file=externalMaskFile, valid_mask=commonMask, ) @@ -1462,12 +1624,14 @@ def prepare_water_mask( frequency="frequencyA", sar_band=DEFAULT_SAR_BAND, commonMask=None, + product_ctx: Optional[NisarProductContext] = None, ): """Prepare a standalone MintPy waterMask.h5 from the GUNW mask.""" print("-" * 50) print(f"preparing water mask file: {outfile}") meta = {key: value for key, value in metadata.items()} + product_ctx = _coerce_product_context(product_ctx, sar_band) # get subset indices geo_ds = read_subset( @@ -1475,14 +1639,18 @@ def prepare_water_mask( bbox, polarization=polarization, frequency=frequency, - sar_band=sar_band, + product_ctx=product_ctx, geometry=True, ) xybbox = geo_ds["xybbox"] if commonMask is None: water_mask_bool = _read_is_land_and_valid_mask( - metaFile, xybbox, polarization, frequency, sar_band + metaFile, + xybbox, + polarization, + frequency, + product_ctx=product_ctx, ) else: water_mask_bool = np.asarray(commonMask, dtype=np.bool_) @@ -1501,7 +1669,7 @@ def prepare_water_mask( externalMaskFile, polarization, frequency, - sar_band, + product_ctx=product_ctx, ) length, width = water_mask_bool.shape @@ -1527,6 +1695,7 @@ def prepare_stack( sar_band=DEFAULT_SAR_BAND, stack_type=None, commonMask=None, + product_ctx: Optional[NisarProductContext] = None, ): """Prepare the input stacks.""" effective_stack_type = _resolve_stack_type(stack_type, outfile) @@ -1534,11 +1703,16 @@ def prepare_stack( print(f"preparing {effective_stack_type} stack file: {outfile}") meta = {key: value for key, value in metadata.items()} + product_ctx = _coerce_product_context(product_ctx, sar_band) num_pair = len(inp_files) print(f"number of inputs/unwrapped interferograms: {num_pair}") missing = _missing_required_paths( - inp_files, effective_stack_type, polarization, frequency, sar_band + inp_files, + effective_stack_type, + polarization, + frequency, + product_ctx=product_ctx, ) if missing: first_file, first_path = missing[0] @@ -1591,7 +1765,7 @@ def prepare_stack( demFile, polarization, frequency, - sar_band, + product_ctx=product_ctx, ) obs = _apply_common_mask_to_observation(obs, commonMask) f["unwrapPhase"][i] = obs["unwrap_phase"] From c1edec1e5d2eb85f41fce70b668cca2fe70bdefa Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 27 May 2026 13:22:14 -0700 Subject: [PATCH 7/9] Fix prep_nisar docstring lint issues --- src/mintpy/prep_nisar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index a543672c2..fb3d28853 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -65,6 +65,7 @@ def _normalize_sar_band(sar_band) -> str: @dataclass(frozen=True) + class NisarProductContext: """Cache the band-specific HDF5 roots and commonly reused dataset paths.""" @@ -75,6 +76,7 @@ class NisarProductContext: processinfo: Dict[str, str] = field(init=False) def __post_init__(self): + """Normalize the band name and precompute shared HDF5 path prefixes.""" sar_band = _normalize_sar_band(self.sar_band) science_root = f"/science/{sar_band}" identification_root = f"{science_root}/identification" From 11c3cc7357ee3be05fef771b5e680ddc6cdc68b4 Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 27 May 2026 13:58:56 -0700 Subject: [PATCH 8/9] Fix prep_nisar class docstring spacing --- src/mintpy/prep_nisar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index fb3d28853..96a2549e1 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -65,8 +65,8 @@ def _normalize_sar_band(sar_band) -> str: @dataclass(frozen=True) - class NisarProductContext: + """Cache the band-specific HDF5 roots and commonly reused dataset paths.""" sar_band: str = DEFAULT_SAR_BAND From 69a9b6b10959f02c67e2dfb4258d6e4f007a89ce Mon Sep 17 00:00:00 2001 From: Emre Havazli Date: Wed, 27 May 2026 15:46:10 -0700 Subject: [PATCH 9/9] Fix prep_nisar Codacy docstring spacing --- src/mintpy/prep_nisar.py | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/src/mintpy/prep_nisar.py b/src/mintpy/prep_nisar.py index 96a2549e1..01080039a 100644 --- a/src/mintpy/prep_nisar.py +++ b/src/mintpy/prep_nisar.py @@ -66,7 +66,6 @@ def _normalize_sar_band(sar_band) -> str: @dataclass(frozen=True) class NisarProductContext: - """Cache the band-specific HDF5 roots and commonly reused dataset paths.""" sar_band: str = DEFAULT_SAR_BAND @@ -227,9 +226,7 @@ def _resolve_frequency( """Resolve and validate the requested NISAR frequency.""" product_ctx = _coerce_product_context(product_ctx, sar_band) resolved = _normalize_frequency(frequency) - datasets = _datasets_for_pol( - polarization, resolved, product_ctx=product_ctx - ) + datasets = _datasets_for_pol(polarization, resolved, product_ctx=product_ctx) with h5py.File(gunw_file, "r") as ds: required_paths = [ @@ -562,9 +559,7 @@ def _read_target_grid( ): """Read the destination EPSG and subset grid axes from a GUNW file.""" product_ctx = _coerce_product_context(product_ctx, sar_band) - datasets = _datasets_for_pol( - polarization, frequency, product_ctx=product_ctx - ) + datasets = _datasets_for_pol(polarization, frequency, product_ctx=product_ctx) with h5py.File(gunw_file, "r") as ds: return ( int(ds[datasets["epsg"]][()]), @@ -613,9 +608,7 @@ def _prepare_radar_grid_interpolation( frequency, product_ctx=product_ctx, ) - rdr_coords = _read_radar_grid_fields( - gunw_file, field_map, product_ctx=product_ctx - ) + rdr_coords = _read_radar_grid_fields(gunw_file, field_map, product_ctx=product_ctx) dem_subset_array = _warp_to_grid_mem( src_path=dem_file, @@ -722,9 +715,7 @@ def _required_paths_for_stack_type( ): """Return HDF5 source datasets needed to build the requested stack.""" product_ctx = _coerce_product_context(product_ctx, sar_band) - datasets = _datasets_for_pol( - polarization, frequency, product_ctx=product_ctx - ) + datasets = _datasets_for_pol(polarization, frequency, product_ctx=product_ctx) processinfo = _processinfo(product_ctx=product_ctx) if stack_type == "ifgram": return [datasets["unw"], datasets["cor"], datasets["connComp"]] @@ -1048,9 +1039,7 @@ def extract_metadata( meta = {} product_ctx = _coerce_product_context(product_ctx, sar_band) - datasets = _datasets_for_pol( - polarization, frequency, product_ctx=product_ctx - ) + datasets = _datasets_for_pol(polarization, frequency, product_ctx=product_ctx) processinfo = _processinfo(product_ctx=product_ctx) with h5py.File(meta_file, "r") as ds: @@ -1061,9 +1050,10 @@ def extract_metadata( xcoord = ds[datasets["xcoord"]][()] ycoord = ds[datasets["ycoord"]][()] meta["EPSG"] = int(ds[datasets["epsg"]][()]) - meta["WAVELENGTH"] = SPEED_OF_LIGHT / ds[ - _center_frequency_path(frequency, product_ctx=product_ctx) - ][()] + meta["WAVELENGTH"] = ( + SPEED_OF_LIGHT + / ds[_center_frequency_path(frequency, product_ctx=product_ctx)][()] + ) meta["ORBIT_DIRECTION"] = ds[processinfo["orbit_direction"]][()].decode("utf-8") meta["POLARIZATION"] = polarization meta["ALOOKS"] = ds[datasets["azimuth_look"]][()] @@ -1198,9 +1188,7 @@ def get_raster_corners( ): """Get the (west, south, east, north) bounds of the image.""" product_ctx = _coerce_product_context(product_ctx, sar_band) - datasets = _datasets_for_pol( - polarization, frequency, product_ctx=product_ctx - ) + datasets = _datasets_for_pol(polarization, frequency, product_ctx=product_ctx) processinfo = _processinfo(product_ctx=product_ctx) with h5py.File(input_file, "r") as ds: xcoord = ds[datasets["xcoord"]][:] @@ -1288,9 +1276,7 @@ def read_subset( ): """Read subset arrays or only geometry bounds for unwrapped products.""" product_ctx = _coerce_product_context(product_ctx, sar_band) - datasets = _datasets_for_pol( - polarization, frequency, product_ctx=product_ctx - ) + datasets = _datasets_for_pol(polarization, frequency, product_ctx=product_ctx) with h5py.File(gunw_file, "r") as ds: xcoord = ds[datasets["xcoord"]][()] ycoord = ds[datasets["ycoord"]][()]