diff --git a/python/packages/nisar/static/ephemeris.py b/python/packages/nisar/static/ephemeris.py index 5fc249d83..ad6eb445e 100644 --- a/python/packages/nisar/static/ephemeris.py +++ b/python/packages/nisar/static/ephemeris.py @@ -4,7 +4,8 @@ from datetime import datetime from nisar.products.readers.attitude import load_attitude_from_xml -from nisar.products.readers.orbit import load_orbit_from_xml +from nisar.products.readers.orbit import load_orbit_from_xml, load_orbit +from nisar.products.readers import SLC import isce3 @@ -13,10 +14,11 @@ def get_cropped_orbit_and_attitude( - orbit_xml_file: str | os.PathLike, - pointing_xml_file: str | os.PathLike, - start_time: str | datetime | None, - end_time: str | datetime | None, + input_file_path: str | os.PathLike | None = None, + orbit_xml_file: str | os.PathLike | None = None, + pointing_xml_file: str | os.PathLike | None = None, + start_time: str | datetime | None = None, + end_time: str | datetime | None = None, *, padding: float = 0.0, ) -> tuple[isce3.core.Orbit, isce3.core.Attitude]: @@ -31,10 +33,12 @@ def get_cropped_orbit_and_attitude( Parameters ---------- - orbit_xml_file : path-like + input_file_path : str or os.PathLike or None + Path to the input NISAR L1 RSLC formatted HDF5 file. + orbit_xml_file : path-like or None Path to the input orbit ephemeris XML file. Must be an existing XML file conforming to the NISAR Orbit Ephemeris Product Specification\ [1]_. - pointing_xml_file : path-like + pointing_xml_file : path-like or None Path to the input radar pointing XML file. Must be an existing XML file conforming to the NISAR Radar Pointing Product Specification\ [2]_. start_time : str or datetime.datetime or None @@ -70,16 +74,48 @@ def get_cropped_orbit_and_attitude( logger = get_logger() - # Load ephemeris data from input XML files. - logger.info(f"Load orbit data from file {orbit_xml_file}") - orbit_full = load_orbit_from_xml(orbit_xml_file) + if orbit_xml_file is not None: + # Load ephemeris data from input XML files. + logger.info(f"Load orbit data from file {orbit_xml_file}") + + if input_file_path is not None: + # Ensure the orbit is referenced to the RSLC radar grid + # reference epoch. + rslc_product = SLC(hdf5file=str(input_file_path)) + rslc_radar_grid = rslc_product.getRadarGrid() + orbit_full = load_orbit(rslc_product, orbit_xml_file, + rslc_radar_grid.ref_epoch) + else: + orbit_full = load_orbit_from_xml(orbit_xml_file) + + elif input_file_path is not None: + # Load ephemeris data from input RSLC HDF5 file. + logger.info(f"Load orbit data from RSLC file {input_file_path}") + rslc_product = SLC(hdf5file=str(input_file_path)) + orbit_full = rslc_product.getOrbit() + else: + raise ValueError( + "Either the RSLC HDF5 or the orbit XML file must be provided" + ) + logger.info( "Original orbit data spans time interval" f" [{orbit_full.start_datetime, orbit_full.end_datetime}]" ) - logger.info(f"Load attitude data from file {pointing_xml_file}") - attitude_full = load_attitude_from_xml(pointing_xml_file) + if pointing_xml_file is not None: + logger.info(f"Load attitude data from file {pointing_xml_file}") + attitude_full = load_attitude_from_xml(pointing_xml_file) + elif input_file_path is not None: + # Load attitude data from input RSLC HDF5 file. + logger.info(f"Load attitude data from RSLC file {input_file_path}") + rslc_product = SLC(hdf5file=str(input_file_path)) + attitude_full = rslc_product.getAttitude() + else: + raise ValueError( + "Either the RSLC HDF5 or the pointing XML file must be provided" + ) + logger.info( "Original attitude data spans time interval" f" [{attitude_full.start_datetime, attitude_full.end_datetime}]" diff --git a/python/packages/nisar/static/product.py b/python/packages/nisar/static/product.py index 4a8176a7c..7352a22a2 100644 --- a/python/packages/nisar/static/product.py +++ b/python/packages/nisar/static/product.py @@ -322,7 +322,7 @@ def populate_grids_group( local_incidence_angle: isce3.io.Raster, line_of_sight_x: isce3.io.Raster, line_of_sight_y: isce3.io.Raster, - water_mask: isce3.io.Raster, + water_mask: isce3.io.Raster | None, rtc_gamma_to_sigma_factor: isce3.io.Raster, rtc_gamma_to_beta_factor: isce3.io.Raster, geo_grid: isce3.product.GeoGridParameters, @@ -439,8 +439,11 @@ def create_raster_layer_dataset(name: str, raster: isce3.io.Raster) -> h5py.Data dem_dataset = create_raster_layer_dataset("digitalElevationModel", reprojected_dem) dem_dataset.attrs["disclaimer"] = to_bytes(dem_disclaimer) - water_mask_dataset = create_raster_layer_dataset("waterMask", water_mask) - water_mask_dataset.attrs["disclaimer"] = to_bytes(water_mask_disclaimer) + if water_mask is not None: + water_mask_dataset = create_raster_layer_dataset("waterMask", + water_mask) + water_mask_dataset.attrs["disclaimer"] = to_bytes( + water_mask_disclaimer) create_raster_layer_dataset("layoverShadowMask", layover_shadow_mask) create_raster_layer_dataset("localIncidenceAngle", local_incidence_angle) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index 110d0361d..313e7b961 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -7,6 +7,7 @@ from collections.abc import Sequence from datetime import datetime, timezone from pathlib import Path +from xmlrpc.client import DateTime import h5py import nisar @@ -33,6 +34,8 @@ import isce3 from isce3.geometry import make_geo_grid_bounding_polygon, load_dem_from_proj from isce3.core import normalize_look_side, normalize_data_interp_method +from nisar.products.readers import SLC +import numpy as np def run_static_layers_workflow(config_file: os.PathLike | str) -> None: @@ -62,6 +65,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: output_params = groups["output"] # Open the input DEM and water mask raster datasets. + input_file_path = dynamic_ancillary_files["input_file_path"] dem_raster_file = dynamic_ancillary_files["dem_raster_file"] water_mask_raster_file = dynamic_ancillary_files["water_mask_raster_file"] logger.info(f"Open DEM raster file {dem_raster_file}") @@ -75,6 +79,8 @@ 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}") + flag_save_water_mask = output_params["layers"]["save_water_mask"] + proj = isce3.core.make_projection(geo_grid.epsg) # dem = isce3.geometry.DEMInterpolator(dem_raster) @@ -86,13 +92,13 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: geo_grid.end_y, geo_grid.start_y, normalize_data_interp_method(dem_interp_method), - proj, - ) + proj) - # 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. + # Load the orbit and attitude data from the input RSLC or 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( + input_file_path=input_file_path, orbit_xml_file=dynamic_ancillary_files["orbit_xml_file"], pointing_xml_file=dynamic_ancillary_files["pointing_xml_file"], **processing_params["ephemeris"], @@ -125,7 +131,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: if rg_spacing is not None and not (rg_spacing > 0.0): raise ValueError(f"Runconfig {rg_spacing=}, must be > 0") - + bounding_box_params = radar_grid_params["bounding_box"] start_datetime_str = bounding_box_params["start_time"] end_datetime_str = bounding_box_params["end_time"] @@ -154,16 +160,53 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: end_time = isce3.core.DateTime(end_datetime_str) if start_range is not None: - logger.info(f' start range [m]: {start_range}') + logger.info(f' start range: {start_range}') if end_range is not None: - logger.info(f' end range [m]: {end_range}') + logger.info(f' end range: {end_range}') if rg_spacing is not None: logger.info(f' range spacing: {rg_spacing}') if az_spacing is not None: logger.info(f' azimuth time interval: {az_spacing}') + # Load radar grid parameters from RSLC (if provided) + if (input_file_path is not None and + (start_time is None or end_time is None or + start_range is None or end_range is None or + rg_spacing is None or az_spacing is None)): + logger.info("Load radar grid parameters from input RSLC file:") + rslc_product = SLC(hdf5file=str(input_file_path)) + rslc_radar_grid = rslc_product.getRadarGrid() + rslc_orbit = rslc_product.getOrbit() + + if start_time is None: + start_time = (rslc_orbit.reference_epoch + + isce3.core.TimeDelta( + rslc_radar_grid.sensing_start)) + logger.info(f" start time: {start_time.isoformat()}") + + if end_time is None: + end_time = (rslc_orbit.reference_epoch + + isce3.core.TimeDelta( + rslc_radar_grid.sensing_stop)) + logger.info(f" end time: {end_time.isoformat()}") + + if start_range is None: + start_range = rslc_radar_grid.starting_range + logger.info(f" start range: {start_range}") + if end_range is None: + end_range = rslc_radar_grid.end_range + logger.info(f" end range: {end_range}") + + if rg_spacing is None: + rg_spacing = rslc_radar_grid.range_pixel_spacing + logger.info(f" range spacing: {rg_spacing}") + + if az_spacing is None: + az_spacing = 1.0 / rslc_radar_grid.prf + logger.info(f" azimuth time interval: {az_spacing}") + if start_time is not None and az_margin != 0.0: start_time -= isce3.core.TimeDelta(az_margin) logger.info(f' start time (adjusted for az. margin) {az_margin}:' @@ -312,15 +355,18 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: max_block_size=geocode_params["max_block_size"], ) - logger.info("Compute re-projected binary water mask layer") - 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"], - ) + if flag_save_water_mask: + logger.info("Compute re-projected binary water mask layer") + 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"], + ) + else: + binary_water_mask = None # Compute radiometric terrain correction (RTC) area normalization # factor (ANF) layers. Results are stored as GeoTIFF files in the @@ -452,11 +498,14 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: dem_description = get_raster_dataset_metadata_item( dem_raster_file, "dem_description", default="(NOT SPECIFIED)" ) - water_mask_description = get_raster_dataset_metadata_item( - water_mask_raster_file, - "water_mask_description", - default="(NOT SPECIFIED)", - ) + if flag_save_water_mask: + water_mask_description = get_raster_dataset_metadata_item( + water_mask_raster_file, + "water_mask_description", + default="(NOT SPECIFIED)", + ) + else: + water_mask_description = "(NOT APPLICABLE)" # Populate the 'grids' group. logger.info("Populate raster layers and grid coordinates in output" diff --git a/share/nisar/defaults/static.yaml b/share/nisar/defaults/static.yaml index 3924cddef..893906743 100644 --- a/share/nisar/defaults/static.yaml +++ b/share/nisar/defaults/static.yaml @@ -2,6 +2,9 @@ runconfig: groups: dynamic_ancillary_file_group: + + # [OPTIONAL] One NISAR L1 RSLC formatted HDF5 file + input_file_path: # [REQUIRED] File path or URL of the input Digital Elevation Model (DEM) raster # file. # Must be an existing file in a GDAL-compatible raster format that spans the @@ -14,12 +17,14 @@ runconfig: # region of interest and conforms to the NISAR Water Mask Product Specification # (JPL D-107710). water_mask_raster_file: - # [REQUIRED] Path to the input orbit ephemeris XML file. + # [REQUIRED] if `input_file_path` is not provided, otherwise [OPTIONAL]. + # Path to the input orbit ephemeris XML file. # Must be an existing XML file conforming to the NISAR Orbit Ephemeris Product # Specification (JPL D-102253) and spanning the desired radar observation time # interval. orbit_xml_file: - # [REQUIRED] Path to the input radar pointing XML file. + # [REQUIRED] if `input_file_path` is not provided, otherwise [OPTIONAL]. + # Path to the input radar pointing XML file. # Must be an existing XML file conforming to the NISAR Radar Pointing Product # Specification (JPL D-102264) and spanning the desired radar observation time # interval. @@ -250,8 +255,8 @@ runconfig: start_time: # [OPTIONAL] Azimuth ending UTC date and time of the end of the radar - # observation, as a string in ISO 8601. format with up to nanosecond precision. - # Must be >= `start_time`. + # observation, as a string in ISO 8601. Format with up to nanosecond + # precision. Must be >= `start_time`. # If not provided, it will be inferred from the specified geographic grid. # If provided, must be a valid azimuth time within the observation time # interval. @@ -434,6 +439,10 @@ runconfig: extraiter: 10 output: + layers: + # [OPTIONAL] Whether to save the water mask layer in the output product. + # Defaults to true. + save_water_mask: true dataset: # [OPTIONAL] Chunk dimensions of 2-D raster datasets in the output product. # Setting `chunk_size` to [-1, -1] will disable chunked storage. diff --git a/share/nisar/schemas/static.yaml b/share/nisar/schemas/static.yaml index 3985f3aa5..017fbef7a 100644 --- a/share/nisar/schemas/static.yaml +++ b/share/nisar/schemas/static.yaml @@ -1,10 +1,11 @@ runconfig: groups: dynamic_ancillary_file_group: + input_file_path: str(required=False) dem_raster_file: str() - water_mask_raster_file: str() - orbit_xml_file: str() - pointing_xml_file: str() + water_mask_raster_file: str(required=False) + orbit_xml_file: str(required=False) + pointing_xml_file: str(required=False) product_path_group: include('product_path_group_options', required=False) primary_executable: include('primary_executable_options', required=False) geometry: include('geometry_options', required=False) @@ -152,9 +153,13 @@ rdr2geo_options: extraiter: int(min=1, required=False) output_options: + layers: include('output_layers_options', required=False) dataset: include('dataset_options', required=False) file: include('file_options', required=False) +output_layers_options: + save_water_mask: bool(required=False) + dataset_options: chunk_size: list(int(min=-1), min=2, max=2, required=False) compression_enabled: bool(required=False)