Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d1c4d7f
disable polarimetric symmetrization by default
gshiroma Jul 3, 2025
0ef09a8
Merge branch 'isce-framework:develop' into develop
gshiroma Jul 9, 2025
2ac2694
revert changes to `symmetrize_cross_pol_channels`
gshiroma Jul 22, 2025
05d7fda
Update GCOV and GSLC specification XMLs
gshiroma Jul 22, 2025
749058d
Revert changes to the GCOV and GSLC specification XMLs
gshiroma Jul 23, 2025
fc8ac53
Merge branch 'isce-framework:develop' into develop
gshiroma Jul 24, 2025
064d073
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 7, 2025
22464ef
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 7, 2025
a75e7f1
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 11, 2025
ab21d36
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 12, 2025
d61d489
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 18, 2025
0af53dd
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 20, 2025
d454e6a
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 21, 2025
6e7e9d1
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 28, 2025
dd80c38
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 29, 2025
63853b2
Merge branch 'isce-framework:develop' into develop
gshiroma Aug 29, 2025
7c7272f
Merge branch 'isce-framework:develop' into develop
gshiroma Sep 2, 2025
dd18a84
Merge branch 'isce-framework:develop' into develop
gshiroma Sep 17, 2025
d43ca22
Merge branch 'isce-framework:develop' into develop
gshiroma Oct 6, 2025
fa38bc0
Make the radar grid azimuth and range spacing parameters configurable…
gshiroma Oct 6, 2025
7b88217
Make the radar grid azimuth and range spacing parameters configurable…
gshiroma Oct 7, 2025
fd7453b
Make the radar grid azimuth and range spacing parameters configurable…
gshiroma Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 115 additions & 79 deletions python/packages/nisar/workflows/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from nisar.static.geo_grid import get_output_geo_grid
from nisar.static.geometry_layers import compute_geometry_layers
from nisar.static.granule_id import form_granule_id
from nisar.static.layover_shadow_mask import compute_geocoded_layover_shadow_mask
from nisar.static.layover_shadow_mask import \
compute_geocoded_layover_shadow_mask
from nisar.static.logging import get_logger, log_elapsed_time
from nisar.static.product import (
build_hdf5_dataset_creation_kwds_dict,
Expand All @@ -25,7 +26,8 @@
)
from nisar.static.rtc_anf_layers import compute_rtc_anf_layers
from nisar.static.runconfig import get_runconfig_params
from nisar.static.util import get_raster_dataset_metadata_item, scratch_directory
from nisar.static.util import get_raster_dataset_metadata_item, \
scratch_directory
from nisar.static.water_mask import binarize_and_reproject_water_mask

import isce3
Expand All @@ -34,7 +36,8 @@

def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
"""
Run the NISAR Static Layers workflow with the specified run configuration file.
Run the NISAR Static Layers workflow with the specified run configuration
file.

Will generate a single STATIC HDF5 granule, as specified in the runconfig.

Expand Down Expand Up @@ -73,43 +76,55 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
geo_grid = get_output_geo_grid(dem_raster=dem_raster, **geo_grid_params)
logger.info(f"Output geo grid: {geo_grid}")

# Parse the orbit and attitude data from the input XML files. Crop the data to the
# time interval of interest to avoid possible geo2rdr convergence errors due to
# ambiguity between orbit periods.
# Parse the orbit and attitude data from the input XML files. Crop the
# data to the time interval of interest to avoid possible geo2rdr
# convergence errors due to ambiguity between orbit periods.
orbit, attitude = get_cropped_orbit_and_attitude(
orbit_xml_file=dynamic_ancillary_files["orbit_xml_file"],
pointing_xml_file=dynamic_ancillary_files["pointing_xml_file"],
**processing_params["ephemeris"],
)

# Get the Doppler centroid associated with the radar grid. NISAR image grids are
# always zero-Doppler.
# Get the Doppler centroid associated with the radar grid. NISAR image
# grids are always zero-Doppler.
img_grid_doppler = isce3.core.LUT2d()

# Estimate the required radar grid spacing necessary to avoid undersampling the
# output geocoded grid.
# Estimate the required radar grid spacing necessary to avoid
# undersampling the output geocoded grid.
# XXX: We deliberately don't pass geo2rdr parameters to either
# `infer_radar_grid_spacing_from_geo_grid()` or `get_bounding_radar_grid()` because
# these functions use `geo2rdr_bracket`, which takes different parameters than the
# legacy `geo2rdr` routine that's used by most of the workflow. Exposing both sets
# of parameters would introduce a lot of additional bookkeeping for seemingly little
# benefit.
# `infer_radar_grid_spacing_from_geo_grid()` or `get_bounding_radar_grid()`
# because these functions use `geo2rdr_bracket`, which takes different
# parameters than the legacy `geo2rdr` routine that's used by most of the
# workflow. Exposing both sets of parameters would introduce a lot of
# additional bookkeeping for seemingly little benefit.
logger.info("Estimate maximum required radar grid spacing")
radar_grid_params = processing_params["radar_grid"]
look_side = radar_grid_params["look_side"]
wavelength = radar_grid_params["wavelength"]
az_spacing, rg_spacing = isce3.geometry.infer_radar_grid_spacing_from_geo_grid(
geo_grid=geo_grid,
dem=dem,
orbit=orbit,
doppler=img_grid_doppler,
look_side=look_side,
wavelength=wavelength,
**radar_grid_params["spacing"],
)

# Compute a radar grid whose footprint on the ground encloses the geocoded grid on
# which each output layer is defined.
radar_grid_spacing_params = radar_grid_params["spacing"]
az_spacing = radar_grid_spacing_params["az_spacing"]
rg_spacing = radar_grid_spacing_params["rg_spacing"]
pts_per_side = radar_grid_spacing_params["pts_per_side"]

if rg_spacing is None or az_spacing is None:
az_spacing_inferred, rg_spacing_inferred = \
isce3.geometry.infer_radar_grid_spacing_from_geo_grid(
geo_grid=geo_grid,
dem=dem,
orbit=orbit,
doppler=img_grid_doppler,
look_side=look_side,
wavelength=wavelength,
pts_per_side=pts_per_side
)
if rg_spacing is None:
rg_spacing = rg_spacing_inferred
if az_spacing is None:
az_spacing = az_spacing_inferred

# Compute a radar grid whose footprint on the ground encloses the geocoded
# grid on which each output layer is defined.
logger.info("Compute a radar grid spanning the region of interest")
radar_grid = isce3.geometry.get_bounding_radar_grid(
geo_grid=geo_grid,
Expand All @@ -133,14 +148,15 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
**processing_params["doppler"],
)

# Create a (possibly temporary) scratch directory to store intermediate files.
# Create a (possibly temporary) scratch directory to store intermediate
# files.
logger.info("Create scratch directory")
with scratch_directory(
product_paths["scratch_dir"], delete=product_paths["delete_scratch_dir"]
) as scratch_dir:
# Compute static geometry layers (height above ellipsoid, line-of-sight X and Y,
# local incidence angle). Results are stored as GeoTIFF files in the scratch
# directory.
# Compute static geometry layers (height above ellipsoid,
# line-of-sight X and Y, local incidence angle). Results are stored as
# GeoTIFF files in the scratch directory.
logger.info("Compute static geometry layers")
geo2rdr_params = processing_params["geo2rdr"]
with log_elapsed_time(logger.info, "Computing static geometry layers"):
Expand All @@ -155,13 +171,16 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
dem_interp_method=dem_interp_method,
geo2rdr_params=geo2rdr_params,
)
reprojected_dem, los_east, los_north, local_inc_angle = geometry_layers
reprojected_dem, los_east, los_north, local_inc_angle = \
geometry_layers

# Compute static mask layers (geocoded layover/shadow mask and water mask).
# Compute static mask layers (geocoded layover/shadow mask and water
# mask).
# Results are stored as GeoTIFF files in the scratch directory.
logger.info("Compute geocoded layover/shadow mask layer")
geocode_params = processing_params["geocode"]
with log_elapsed_time(logger.info, "Computing geocoded layover/shadow mask"):
with log_elapsed_time(logger.info,
"Computing geocoded layover/shadow mask"):
layover_shadow_mask = compute_geocoded_layover_shadow_mask(
radar_grid=radar_grid,
orbit=orbit,
Expand All @@ -179,51 +198,57 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
)

logger.info("Compute re-projected binary water mask layer")
with log_elapsed_time(logger.info, "Computing re-projected binary water mask"):
with log_elapsed_time(logger.info,
"Computing re-projected binary water mask"):
binary_water_mask = binarize_and_reproject_water_mask(
water_distance_raster_file=water_mask_raster_file,
geo_grid=geo_grid,
scratch_dir=scratch_dir,
**processing_params["water_mask"],
)

# Compute radiometric terrain correction (RTC) area normalization factor (ANF)
# layers. Results are stored as GeoTIFF files in the scratch directory.
# Compute radiometric terrain correction (RTC) area normalization
# factor (ANF) layers. Results are stored as GeoTIFF files in the
# scratch directory.
logger.info("Compute RTC area normalization factor layers")
rtc_params = processing_params["rtc"]
with log_elapsed_time(logger.info, "Computing RTC area normalization layers"):
gamma0_to_beta0_factor, gamma0_to_sigma0_factor = compute_rtc_anf_layers(
radar_grid=radar_grid,
orbit=orbit,
native_doppler=native_doppler,
img_grid_doppler=img_grid_doppler,
geo_grid=geo_grid,
dem_raster=dem_raster,
scratch_dir=scratch_dir,
dem_interp_method=dem_interp_method,
geo2rdr_params=geo2rdr_params,
**geocode_params,
**rtc_params,
)
with log_elapsed_time(logger.info,
"Computing RTC area normalization layers"):
gamma0_to_beta0_factor, gamma0_to_sigma0_factor = \
compute_rtc_anf_layers(
radar_grid=radar_grid,
orbit=orbit,
native_doppler=native_doppler,
img_grid_doppler=img_grid_doppler,
geo_grid=geo_grid,
dem_raster=dem_raster,
scratch_dir=scratch_dir,
dem_interp_method=dem_interp_method,
geo2rdr_params=geo2rdr_params,
**geocode_params,
**rtc_params,
)

# Infer the orbit pass direction from the orbit velocity vectors.
orbit_pass_direction = isce3.core.get_orbit_pass_direction(orbit)

# Pop 'product_counter' from the dict. This parameter is used to form the
# granule ID but doesn't correspond to any dataset in the 'identification' group
# of the product. The other dict contents will be passed as keyword arguments to
# `populate_identification_group()` below.
# Pop 'product_counter' from the dict. This parameter is used to form
# the granule ID but doesn't correspond to any dataset in the
# 'identification' group of the product. The other dict contents will
# be passed as keyword arguments to `populate_identification_group()`
# below.
product_counter = primary_executable_params.pop("product_counter")

# Get `validity_start_datetime` from the input parameters as a
# `datetime.datetime` object. If it was passed as a non-quoted string in ISO
# 8601 format, `ruamel.yaml` will have already converted it. Otherwise, manually
# convert it here.
# `datetime.datetime` object. If it was passed as a non-quoted string
# in ISO 8601 format, `ruamel.yaml` will have already converted it.
# Otherwise, manually convert it here.
validity_start_datetime = primary_executable_params.pop(
"validity_start_datetime"
)
if not isinstance(validity_start_datetime, datetime):
validity_start_datetime = datetime.fromisoformat(validity_start_datetime)
validity_start_datetime = \
datetime.fromisoformat(validity_start_datetime)

# Get the unique ID of the granule based on the input parameters.
radar_band = primary_executable_params["radar_band"]
Expand All @@ -237,7 +262,8 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
x_posting=abs(geo_grid.spacing_x),
y_posting=abs(geo_grid.spacing_y),
validity_start_datetime=validity_start_datetime,
composite_release_id=primary_executable_params["composite_release_id"],
composite_release_id=primary_executable_params[
"composite_release_id"],
processing_center=primary_executable_params["processing_center"],
product_counter=product_counter,
**geometry_params,
Expand All @@ -262,28 +288,34 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
# Populate global attributes in the root group of the file.
logger.info("Populate global HDF5 attributes")
product_spec = nisar.products.get_product_spec("STATIC")
nisar.products.populate_global_attrs_from_spec(hdf5_file, product_spec)
nisar.products.populate_global_attrs_from_spec(hdf5_file,
product_spec)

# Get the current processing datetime (truncated to integer seconds
# precision).
processing_datetime = datetime.now(timezone.utc).replace(microsecond=0)
processing_datetime = \
datetime.now(timezone.utc).replace(microsecond=0)

# XXX: It's not really obvious what should go in the `zeroDopplerStartTime`
# and `zeroDopplerEndTime` datasets in the 'identification' group. For now,
# we'll use the start & stop time of the radar grid, which is roughly
# XXX: It's not really obvious what should go in the
# `zeroDopplerStartTime` and `zeroDopplerEndTime` datasets in the
# 'identification' group. For now, we'll use the start & stop time
# of the radar grid, which is roughly
# analogous what they represent in other NISAR L2 products.
img_grid_start_datetime = radar_grid.ref_epoch + isce3.core.TimeDelta(
radar_grid.sensing_start
)
img_grid_end_datetime = radar_grid.ref_epoch + isce3.core.TimeDelta(
radar_grid.sensing_stop
)
img_grid_start_datetime = (radar_grid.ref_epoch +
isce3.core.TimeDelta(
radar_grid.sensing_start))
img_grid_end_datetime = (radar_grid.ref_epoch +
isce3.core.TimeDelta(
radar_grid.sensing_stop))

# Populate the 'identification' group.
logger.info("Populate identification metadata in output HDF5 file")
instrument_group = hdf5_file.create_group(f"/science/{radar_band}SAR")
identification_group = instrument_group.create_group("identification")
bounding_polygon = make_geo_grid_bounding_polygon(geo_grid, dem=dem)
instrument_group = \
hdf5_file.create_group(f"/science/{radar_band}SAR")
identification_group = \
instrument_group.create_group("identification")
bounding_polygon = make_geo_grid_bounding_polygon(geo_grid,
dem=dem)
populate_identification_group(
identification_group=identification_group,
product_spec=product_spec,
Expand Down Expand Up @@ -311,13 +343,15 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None:
)

# Populate the 'grids' group.
logger.info("Populate raster layers and grid coordinates in output HDF5")
logger.info("Populate raster layers and grid coordinates in output"
" HDF5")
grids_group = instrument_group.create_group("STATIC/grids")
dataset_creation_kwds = build_hdf5_dataset_creation_kwds_dict(
dataset_shape=(geo_grid.length, geo_grid.width),
**output_params["dataset"]
)
with log_elapsed_time(logger.info, "Writing raster layers to output HDF5"):
with log_elapsed_time(logger.info, "Writing raster layers to"
" output HDF5"):
populate_grids_group(
grids_group=grids_group,
product_spec=product_spec,
Expand Down Expand Up @@ -360,18 +394,20 @@ def main(args: Sequence[str] | None = None) -> None:
Parameters
----------
args : sequence of str or None, optional
The list of arguments. If None, the argument list is taken from `sys.argv`.
Defaults to None.
The list of arguments. If None, the argument list is taken from
`sys.argv`. Defaults to None.
"""
# Setup the argument parser.
parser = argparse.ArgumentParser(description="Run the NISAR Static Layers workflow")
parser = argparse.ArgumentParser(
description="Run the NISAR Static Layers workflow")
parser.add_argument(
"config_file",
type=Path,
help="Run configuration YAML file for the STATIC workflow",
)

# Parse the arguments and convert the result to a dict of keyword arguments.
# Parse the arguments and convert the result to a dict of keyword
# arguments.
kwargs = vars(parser.parse_args(args))

# Run the workflow with the unpacked keyword arguments.
Expand Down
10 changes: 10 additions & 0 deletions share/nisar/defaults/static.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ runconfig:
# Defaults to 0.24.
wavelength: 0.24
spacing:
# [OPTIONAL] Azimuth interval, in seconds. Equivalent to the Pulse
# Repetition Interval (PRI).
# If not provided, it will be inferred from the specified geographic
# grid. If provided, must be a positive value.
az_spacing:
# [OPTIONAL] Slant-range spacing, in meters, of the radar grid.
# Must be a positive value.
# If not provided, it will be inferred from the specified geographic
# grid. If provided, must be a positive value.
rg_spacing:
# [OPTIONAL] Side length of the NxN grid of samples used to estimate the
# required radar grid pixel spacing necessary to avoid undersampling the
# output geocoded grid.
Expand Down
2 changes: 2 additions & 0 deletions share/nisar/schemas/static.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ radar_grid_options:
bounding_box: include('radar_grid_bounding_box_options', required=False)

radar_grid_spacing_options:
az_spacing: num(min=0.0, required=False)
rg_spacing: num(min=0.0, required=False)
pts_per_side: int(min=2, required=False)

radar_grid_bounding_box_options:
Expand Down