From d1c4d7f93fe3f9ec11bf11e32259ea02b21a0dcd Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Thu, 3 Jul 2025 11:30:42 -0700 Subject: [PATCH 01/16] disable polarimetric symmetrization by default --- share/nisar/defaults/gcov.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/nisar/defaults/gcov.yaml b/share/nisar/defaults/gcov.yaml index db0c71352..3b9da8a89 100644 --- a/share/nisar/defaults/gcov.yaml +++ b/share/nisar/defaults/gcov.yaml @@ -177,7 +177,7 @@ runconfig: # HV and VH), otherwise, the flag is ignored. # If enabled, the output product's "HV" dataset will contain symmetrized # HV/VH data and the "VH" dataset will be omitted from the output. - symmetrize_cross_pol_channels: True + symmetrize_cross_pol_channels: False # TODO OPTIONAL - Only checked when internet access is available dem_download: From 2ac26942f19a6ff2e3145a96ca52044d09f867ea Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Tue, 22 Jul 2025 13:58:03 -0700 Subject: [PATCH 02/16] revert changes to `symmetrize_cross_pol_channels` --- share/nisar/defaults/gcov.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/nisar/defaults/gcov.yaml b/share/nisar/defaults/gcov.yaml index 3b9da8a89..db0c71352 100644 --- a/share/nisar/defaults/gcov.yaml +++ b/share/nisar/defaults/gcov.yaml @@ -177,7 +177,7 @@ runconfig: # HV and VH), otherwise, the flag is ignored. # If enabled, the output product's "HV" dataset will contain symmetrized # HV/VH data and the "VH" dataset will be omitted from the output. - symmetrize_cross_pol_channels: False + symmetrize_cross_pol_channels: True # TODO OPTIONAL - Only checked when internet access is available dem_download: From 05d7fda0ff21704541179bfc5b0fb5415ebae1b6 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Tue, 22 Jul 2025 16:01:58 -0700 Subject: [PATCH 03/16] Update GCOV and GSLC specification XMLs --- .../nisar/products/XML/L2/nisar_L2_GCOV.xml | 291 ++++++++++------ .../nisar/products/XML/L2/nisar_L2_GSLC.xml | 321 +++++++++++------- 2 files changed, 371 insertions(+), 241 deletions(-) diff --git a/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml b/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml index 021b48b6a..a9e931bdb 100644 --- a/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml +++ b/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml @@ -105,7 +105,7 @@ + shape="numberOfObservations"> List of planned datatakes included in the product @@ -127,13 +127,19 @@ List of frequency layers available in the product - + List of each input raw dataset's observation mode + + - Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 - + Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 + @@ -182,7 +188,7 @@ name="/science/LSAR/identification/processingType" shape="scalar"> Nominal (or) Urgent (or) Custom (or) Undefined + lang="en">Processing pipeline used to generate this granule. "Nominal": standard production system; "Urgent": time-sensitive processing in response to urgent response events; "Custom": user-initiated processing outside the nominal production system X coordinates in specified projection Y coordinates in specified projection @@ -329,7 +337,7 @@ valid_min="0" _FillValue="nan" grid_mapping="projection" - units="1">Radiometric terrain correction factor to normalize GCOV terms from gamma0 to sigma0 + units="1">Radiometric terrain correction (RTC) scaling factor to normalize backscatter coefficients from gamma0 to sigma0, accounting for local terrain Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value `0`. GCOV pixels outside of the radar acquisition extent are filled with the value `255` + grid_mapping="projection">Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value 0. GCOV pixels outside of the radar acquisition extent are filled with the value 255 X coordinates in specified projection Y coordinates in specified projection @@ -706,7 +716,7 @@ valid_min="0" _FillValue="nan" grid_mapping="projection" - units="1">Radiometric terrain correction factor to normalize GCOV terms from gamma0 to sigma0 + units="1">Radiometric terrain correction (RTC) scaling factor to normalize backscatter coefficients from gamma0 to sigma0, accounting for local terrain Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value `0`. GCOV pixels outside of the radar acquisition extent are filled with the value `255` + grid_mapping="projection">Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value 0. GCOV pixels outside of the radar acquisition extent are filled with the value 255 East component of unit vector of LOS from target to sensor + units="1">East component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location North component of unit vector of LOS from target to sensor + units="1">North component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location East component of unit vector along ground track + units="1">East component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) North component of unit vector along ground track + units="1">North component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) X coordinates in specified projection X coordinates in specified projection - - + + Product map grid projection: EPSG code, with additional projection information as HDF5 Attributes + + + Y coordinates in specified projection + + + X coordinates in specified projection + Crosstalk in H-transmit channel expressed as ratio txV / txH Crosstalk in V-transmit channel expressed as ratio txH / txV Crosstalk in H-receive channel expressed as ratio rxV / rxH Crosstalk in V-receive channel expressed as ratio rxH / rxV + + @@ -2437,14 +2519,14 @@ Reference Terrain Height as a function of map coordinates + units="meters">Reference terrain height as a function of map coordinates scalar values - - - number of datatakes in product - - - number of observations in product + lang="en">Number of observations in product @@ -3372,28 +3445,22 @@ Shape of calibration LUTs + lang="en">Shape of real-valued calibration LUTs - Shape of antenna pattern datasets + lang="en">Shape of complex-valued calibration LUTs - - Shape of crosstalk datasets - - + shape="numberOfObservations"> List of planned datatakes included in the product @@ -127,13 +127,19 @@ List of frequency layers available in the product - + List of each input raw dataset's observation mode + + - Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 - + Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 + @@ -182,7 +188,7 @@ name="/science/LSAR/identification/processingType" shape="scalar"> Nominal (or) Urgent (or) Custom (or) Undefined + lang="en">Processing pipeline used to generate this granule. "Nominal": standard production system; "Urgent": time-sensitive processing in response to urgent response events; "Custom": user-initiated processing outside the nominal production system Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of `0` indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value `255`. + _FillValue="255">Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of 0 indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value 255. Focused SLC image (HH) @@ -373,7 +379,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image HV" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (HV) @@ -393,7 +399,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VH" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VH) @@ -413,7 +419,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VV" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VV) @@ -433,7 +439,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RH" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RH) @@ -453,7 +459,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RV" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RV) @@ -538,14 +544,14 @@ Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of `0` indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value `255`. + _FillValue="255">Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of 0 indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value 255. Focused SLC image (HH) @@ -626,7 +632,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image HV" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (HV) @@ -646,7 +652,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VH" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VH) @@ -666,7 +672,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VV" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VV) @@ -686,7 +692,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RH" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RH) @@ -706,7 +712,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RVH" - grid_mapping='projection' + grid_mapping="projection" DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RV) @@ -832,7 +838,7 @@ _FillValue="nan" grid_mapping="projection" long_name="LOS unit vector X" - units="1">East component of unit vector of LOS from target to sensor + units="1">East component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location North component of unit vector of LOS from target to sensor + units="1">North component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location East component of unit vector along ground track + units="1">East component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) North component of unit vector along ground track + units="1">North component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) X coordinates in specified projection X coordinates in specified projection - - + + Product map grid projection: EPSG code, with additional projection information as HDF5 Attributes + + + Y coordinates in specified projection + + + X coordinates in specified projection + Crosstalk in H-transmit channel expressed as ratio txV / txH Crosstalk in V-transmit channel expressed as ratio txH / txV Crosstalk in H-receive channel expressed as ratio rxV / rxH Crosstalk in V-receive channel expressed as ratio rxH / rxV + + @@ -2096,7 +2174,7 @@ lang="en" _FillValue="nan" grid_mapping="projection" - units="meters">Reference Terrain Height as a function of geographical location + units="meters">Reference terrain height as a function of map coordinates scalar values - - - number of datatakes in product - - - number of observations in product + lang="en">Number of observations in product @@ -3116,28 +3185,22 @@ Shape of calibration LUTs + lang="en">Shape of real-valued calibration LUTs - Shape of antenna pattern datasets + lang="en">Shape of complex-valued calibration LUTs - - Shape of crosstalk datasets - - Number of input L1 SLC granules + name="numberOfInputL1Files"/> From 749058db5115479f727ca055d828370ecd1ec07e Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Wed, 23 Jul 2025 11:45:11 -0700 Subject: [PATCH 04/16] Revert changes to the GCOV and GSLC specification XMLs --- .../nisar/products/XML/L2/nisar_L2_GCOV.xml | 291 ++++++---------- .../nisar/products/XML/L2/nisar_L2_GSLC.xml | 321 +++++++----------- 2 files changed, 241 insertions(+), 371 deletions(-) diff --git a/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml b/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml index a9e931bdb..021b48b6a 100644 --- a/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml +++ b/python/packages/nisar/products/XML/L2/nisar_L2_GCOV.xml @@ -105,7 +105,7 @@ + shape="numberOfDatatakes"> List of planned datatakes included in the product @@ -127,19 +127,13 @@ List of frequency layers available in the product - - List of each input raw dataset's observation mode - - - Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 - + Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 + @@ -188,7 +182,7 @@ name="/science/LSAR/identification/processingType" shape="scalar"> Processing pipeline used to generate this granule. "Nominal": standard production system; "Urgent": time-sensitive processing in response to urgent response events; "Custom": user-initiated processing outside the nominal production system + lang="en">Nominal (or) Urgent (or) Custom (or) Undefined X coordinates in specified projection Y coordinates in specified projection @@ -337,7 +329,7 @@ valid_min="0" _FillValue="nan" grid_mapping="projection" - units="1">Radiometric terrain correction (RTC) scaling factor to normalize backscatter coefficients from gamma0 to sigma0, accounting for local terrain + units="1">Radiometric terrain correction factor to normalize GCOV terms from gamma0 to sigma0 Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value 0. GCOV pixels outside of the radar acquisition extent are filled with the value 255 + grid_mapping="projection">Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value `0`. GCOV pixels outside of the radar acquisition extent are filled with the value `255` X coordinates in specified projection Y coordinates in specified projection @@ -716,7 +706,7 @@ valid_min="0" _FillValue="nan" grid_mapping="projection" - units="1">Radiometric terrain correction (RTC) scaling factor to normalize backscatter coefficients from gamma0 to sigma0, accounting for local terrain + units="1">Radiometric terrain correction factor to normalize GCOV terms from gamma0 to sigma0 Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value 0. GCOV pixels outside of the radar acquisition extent are filled with the value 255 + grid_mapping="projection">Mask indicating the subswath number associated with valid GCOV samples. A GCOV sample is only considered valid if it is generated from fully-focused radar samples. If at least one radar sample in the averaging set is partially focused or invalid, the corresponding mask pixel will contain the value `0`. GCOV pixels outside of the radar acquisition extent are filled with the value `255` East component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location + units="1">East component of unit vector of LOS from target to sensor North component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location + units="1">North component of unit vector of LOS from target to sensor East component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) + units="1">East component of unit vector along ground track North component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) + units="1">North component of unit vector along ground track X coordinates in specified projection X coordinates in specified projection + + - - Product map grid projection: EPSG code, with additional projection information as HDF5 Attributes - - - Y coordinates in specified projection - - - X coordinates in specified projection - Crosstalk in H-transmit channel expressed as ratio txV / txH Crosstalk in V-transmit channel expressed as ratio txH / txV Crosstalk in H-receive channel expressed as ratio rxV / rxH Crosstalk in V-receive channel expressed as ratio rxH / rxV - - @@ -2519,14 +2437,14 @@ Reference terrain height as a function of map coordinates + units="meters">Reference Terrain Height as a function of map coordinates scalar values + + + number of datatakes in product + + + Number of observations in product + lang="en">number of observations in product @@ -3445,22 +3372,28 @@ Shape of real-valued calibration LUTs + lang="en">Shape of calibration LUTs - Shape of complex-valued calibration LUTs + lang="en">Shape of antenna pattern datasets + + Shape of crosstalk datasets + + + shape="numberOfDatatakes"> List of planned datatakes included in the product @@ -127,19 +127,13 @@ List of frequency layers available in the product - - List of each input raw dataset's observation mode - - - Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 - + Indicates if the radar operation mode is a diagnostic mode (1-2) or DBFed science (0): 0, 1, or 2 + @@ -188,7 +182,7 @@ name="/science/LSAR/identification/processingType" shape="scalar"> Processing pipeline used to generate this granule. "Nominal": standard production system; "Urgent": time-sensitive processing in response to urgent response events; "Custom": user-initiated processing outside the nominal production system + lang="en">Nominal (or) Urgent (or) Custom (or) Undefined Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of 0 indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value 255. + _FillValue="255">Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of `0` indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value `255`. Focused SLC image (HH) @@ -379,7 +373,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image HV" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (HV) @@ -399,7 +393,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VH" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VH) @@ -419,7 +413,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VV" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VV) @@ -439,7 +433,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RH" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RH) @@ -459,7 +453,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RV" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RV) @@ -544,14 +538,14 @@ Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of 0 indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value 255. + _FillValue="255">Mask indicating the subswath number representing valid GSLC samples. Each GSLC pixel is assumed valid if all the pixels in the interpolation window are fully focused in the input RSLC. A value of `0` indicates that at least one RSLC pixel in the interpolation window is partially focused or invalid. Pixels outside of the radar acquisition extent are filled with the value `255`. Focused SLC image (HH) @@ -632,7 +626,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image HV" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (HV) @@ -652,7 +646,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VH" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VH) @@ -672,7 +666,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image VV" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (VV) @@ -692,7 +686,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RH" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RH) @@ -712,7 +706,7 @@ mean_real_value="Arithmetic average of the real part of the numeric data points" sample_stddev_real="Standard deviation of the real part of the numeric data points" long_name="Geocoded single-look complex image RVH" - grid_mapping="projection" + grid_mapping='projection' DIMENSION_LIST="HDF5 internal attribute" _FillValue="(nan+nan*j)" units="1">Focused SLC image (RV) @@ -838,7 +832,7 @@ _FillValue="nan" grid_mapping="projection" long_name="LOS unit vector X" - units="1">East component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location + units="1">East component of unit vector of LOS from target to sensor North component of the line-of-sight (LOS) unit vector, defined from the target to the sensor, expressed in the east-north-up (ENU) coordinate system with its origin at the target location + units="1">North component of unit vector of LOS from target to sensor East component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) + units="1">East component of unit vector along ground track North component of the along-track unit vector at the target location, expressed in the east-north-up (ENU) coordinate system and projected onto the horizontal plane (i.e., excluding the up component) + units="1">North component of unit vector along ground track X coordinates in specified projection X coordinates in specified projection + + - - Product map grid projection: EPSG code, with additional projection information as HDF5 Attributes - - - Y coordinates in specified projection - - - X coordinates in specified projection - Crosstalk in H-transmit channel expressed as ratio txV / txH Crosstalk in V-transmit channel expressed as ratio txH / txV Crosstalk in H-receive channel expressed as ratio rxV / rxH Crosstalk in V-receive channel expressed as ratio rxH / rxV - - @@ -2174,7 +2096,7 @@ lang="en" _FillValue="nan" grid_mapping="projection" - units="meters">Reference terrain height as a function of map coordinates + units="meters">Reference Terrain Height as a function of geographical location scalar values + + + number of datatakes in product + + + Number of observations in product + lang="en">number of observations in product @@ -3185,22 +3116,28 @@ Shape of real-valued calibration LUTs + lang="en">Shape of calibration LUTs - Shape of complex-valued calibration LUTs + lang="en">Shape of antenna pattern datasets + + Shape of crosstalk datasets + + Number of input L1 SLC granules + name="numberOfInputL0BFiles"/> From fa38bc003b79929a469fe53d612a60ac513f821c Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Mon, 6 Oct 2025 15:29:03 -0700 Subject: [PATCH 05/16] Make the radar grid azimuth and range spacing parameters configurable in the static layers runconfig --- python/packages/nisar/workflows/static.py | 210 +++++++++++++--------- share/nisar/defaults/static.yaml | 10 ++ share/nisar/schemas/static.yaml | 2 + 3 files changed, 141 insertions(+), 81 deletions(-) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index e7165519b..736659497 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -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, @@ -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 @@ -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. @@ -65,51 +68,75 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: # Construct a DEM interpolator. dem_interp_method = processing_params["dem"]["interp_method"] - dem = isce3.geometry.DEMInterpolator(dem_raster) - dem.interp_method = dem_interp_method # Construct the output geocoded coordinate grid. geo_grid_params = processing_params["geo_grid"] 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"] + + logger.info(f'az_spacing from runconfig: {az_spacing}') + logger.info(f'rg_spacing from runconfig: {rg_spacing}') + 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, + **radar_grid_params["spacing"], + ) + logger.info(f'az_spacing from inferred: {az_spacing_inferred}') + logger.info(f'rg_spacing from inferred: {rg_spacing_inferred}') + + 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, + **radar_grid_params["spacing"], + ) + 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, @@ -133,14 +160,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"): @@ -155,13 +183,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, @@ -179,7 +210,8 @@ 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, @@ -187,43 +219,48 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: **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"] @@ -237,7 +274,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, @@ -262,28 +300,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, @@ -311,13 +355,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, @@ -360,18 +406,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. diff --git a/share/nisar/defaults/static.yaml b/share/nisar/defaults/static.yaml index 83077376f..791536cf2 100644 --- a/share/nisar/defaults/static.yaml +++ b/share/nisar/defaults/static.yaml @@ -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. diff --git a/share/nisar/schemas/static.yaml b/share/nisar/schemas/static.yaml index 7d20a5670..d0110ac4f 100644 --- a/share/nisar/schemas/static.yaml +++ b/share/nisar/schemas/static.yaml @@ -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: From 7b8821741bf73f56a12095138b62080ed71f72d7 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Tue, 7 Oct 2025 08:57:04 -0700 Subject: [PATCH 06/16] Make the radar grid azimuth and range spacing parameters configurable in the static layers runconfig (2) --- python/packages/nisar/workflows/static.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index 736659497..7cce7de60 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -68,6 +68,8 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: # Construct a DEM interpolator. dem_interp_method = processing_params["dem"]["interp_method"] + dem = isce3.geometry.DEMInterpolator(dem_raster) + dem.interp_method = dem_interp_method # Construct the output geocoded coordinate grid. geo_grid_params = processing_params["geo_grid"] @@ -103,6 +105,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: 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"] logger.info(f'az_spacing from runconfig: {az_spacing}') logger.info(f'rg_spacing from runconfig: {rg_spacing}') @@ -115,6 +118,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: look_side=look_side, wavelength=wavelength, **radar_grid_params["spacing"], + pts_per_side=pts_per_side, ) logger.info(f'az_spacing from inferred: {az_spacing_inferred}') logger.info(f'rg_spacing from inferred: {rg_spacing_inferred}') From fd7453ba6203e08b8399ff8a7cc37e166add3146 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Mon, 10 Nov 2025 15:47:38 -0800 Subject: [PATCH 07/16] Make the radar grid azimuth and range spacing parameters configurable in the static layers runconfig --- python/packages/nisar/workflows/static.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index 7cce7de60..ebf58e9eb 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -107,22 +107,6 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: rg_spacing = radar_grid_spacing_params["rg_spacing"] pts_per_side = radar_grid_spacing_params["pts_per_side"] - logger.info(f'az_spacing from runconfig: {az_spacing}') - logger.info(f'rg_spacing from runconfig: {rg_spacing}') - 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, - **radar_grid_params["spacing"], - pts_per_side=pts_per_side, - ) - logger.info(f'az_spacing from inferred: {az_spacing_inferred}') - logger.info(f'rg_spacing from inferred: {rg_spacing_inferred}') - 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( @@ -132,7 +116,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: doppler=img_grid_doppler, look_side=look_side, wavelength=wavelength, - **radar_grid_params["spacing"], + pts_per_side=pts_per_side ) if rg_spacing is None: rg_spacing = rg_spacing_inferred From 4ee53ebedeb37a692847194bf67d1459f0a56a45 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Tue, 20 Jan 2026 16:46:49 -0800 Subject: [PATCH 08/16] Expose sensing start/end times and starting/ending ranges in the STATIC workflow --- .../isce3/geometry/bounding_radar_grid.py | 5 ++ python/packages/nisar/workflows/static.py | 66 +++++++++++++++---- share/nisar/defaults/static.yaml | 24 +++++++ share/nisar/schemas/static.yaml | 4 ++ 4 files changed, 86 insertions(+), 13 deletions(-) diff --git a/python/packages/isce3/geometry/bounding_radar_grid.py b/python/packages/isce3/geometry/bounding_radar_grid.py index d583da79f..2c06b6247 100644 --- a/python/packages/isce3/geometry/bounding_radar_grid.py +++ b/python/packages/isce3/geometry/bounding_radar_grid.py @@ -303,6 +303,11 @@ def get_bounding_radar_grid( Azimuth time spacing of the output grid, in seconds. Must be > 0. rg_spacing : float Slant range spacing of the output grid, in meters. Must be > 0. + sensing_start : float + The sensing start time of the input `geo_grid`, in seconds since the epoch of + `orbit`. + starting_range : float + The starting slant range of the input `geo_grid`, in meters. orbit : isce3.core.Orbit The trajectory of the radar antenna phase center over a time interval that includes the observation times of each point in `geo_grid` at each height diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index ebf58e9eb..eb74fd19d 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -32,6 +32,8 @@ import isce3 from isce3.geometry import make_geo_grid_bounding_polygon +from isce3.core import normalize_look_side +import numpy as np def run_static_layers_workflow(config_file: os.PathLike | str) -> None: @@ -105,8 +107,15 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: 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"] + bounding_box_params = radar_grid_params["bounding_box"] + sensing_start = bounding_box_params["sensing_start"] + sensing_end = bounding_box_params["sensing_end"] + starting_range = bounding_box_params["starting_range"] + ending_range = bounding_box_params["ending_range"] + 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( @@ -123,19 +132,50 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: 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, - az_spacing=az_spacing, - rg_spacing=rg_spacing, - orbit=orbit, - look_side=look_side, - wavelength=wavelength, - doppler=img_grid_doppler, - **radar_grid_params["bounding_box"], - ) + if (sensing_start is not None and starting_range is not None and + sensing_end is not None and ending_range is not None): + + # Use the provided bounding box parameters. + # Construct the radar grid. + # Divide `num` by `den`, rounded up to the next smallest integer. + def ceil_divide(num: float, den: float) -> int: + return int(np.ceil(num / den)) + num_az = ceil_divide(sensing_end - sensing_start, az_spacing) + 1 + num_rg = ceil_divide(ending_range - starting_range, rg_spacing) + 1 + + radar_grid = isce3.product.RadarGridParameters( + sensing_start=sensing_start, + wavelength=wavelength, + prf=1.0 / az_spacing, + starting_range=starting_range, + range_pixel_spacing=rg_spacing, + lookside=normalize_look_side(look_side), + length=num_az, + width=num_rg, + ref_epoch=orbit.reference_epoch, + ) + elif (sensing_start is not None or starting_range is not None or + sensing_end is not None or ending_range is not None): + raise ValueError( + "If specifying radar grid bounding box parameters, must provide" + " all of 'sensing_start', 'starting_range', 'sensing_end', and" + " 'ending_range'" + ) + else: + + # 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, + az_spacing=az_spacing, + rg_spacing=rg_spacing, + orbit=orbit, + look_side=look_side, + wavelength=wavelength, + doppler=img_grid_doppler, + **radar_grid_params["bounding_box"], + ) logger.info(f"Using radar grid: {radar_grid}") # Get the native Doppler LUT. diff --git a/share/nisar/defaults/static.yaml b/share/nisar/defaults/static.yaml index 791536cf2..028e1e073 100644 --- a/share/nisar/defaults/static.yaml +++ b/share/nisar/defaults/static.yaml @@ -242,6 +242,30 @@ runconfig: # Defaults to 5. pts_per_side: 5 bounding_box: + # [OPTIONAL] Azimuth start time, in seconds since the epoch, of the first row of + # the radar grid. + # 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. + sensing_start: + + # [OPTIONAL] Azimuth ending time, in seconds since the epoch, of the last row of + # the radar grid. + # 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. + sensing_end: + + # [OPTIONAL] Starting range, in meters, of the first column of the radar grid. + # If not provided, it will be inferred from the specified geographic grid. + # If provided, must be a positive value. + starting_range: + + # [OPTIONAL] Ending range, in meters, of the last column of the radar grid. + # If not provided, it will be inferred from the specified geographic grid. + # If provided, must be a positive value. + ending_range: + # [OPTIONAL] Lower bound on the height of targets within the region of interest, # in meters above the reference ellipsoid of the output product. # Used to estimate the bounds of a radar grid that spans the region of interest. diff --git a/share/nisar/schemas/static.yaml b/share/nisar/schemas/static.yaml index d0110ac4f..08401be1d 100644 --- a/share/nisar/schemas/static.yaml +++ b/share/nisar/schemas/static.yaml @@ -99,6 +99,10 @@ radar_grid_spacing_options: pts_per_side: int(min=2, required=False) radar_grid_bounding_box_options: + sensing_start: num(required=False) + sensing_end: num(required=False) + starting_range: num(required=False) + ending_range: num(required=False) min_height: num(required=False) max_height: num(required=False) pts_per_edge: int(min=2, required=False) From 31a98f0e626a2f95253199623a3cbcb0ce7984ef Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Wed, 21 Jan 2026 11:42:43 -0800 Subject: [PATCH 09/16] Expose sensing start/end times and starting/ending ranges in the STATIC workflow --- python/packages/nisar/workflows/static.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index eb74fd19d..3c081dafb 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -115,6 +115,11 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: sensing_end = bounding_box_params["sensing_end"] starting_range = bounding_box_params["starting_range"] ending_range = bounding_box_params["ending_range"] + min_height = bounding_box_params["min_height"] + max_height = bounding_box_params["max_height"] + pts_per_edge = bounding_box_params["pts_per_edge"] + az_margin = bounding_box_params["az_margin"] + rg_margin = bounding_box_params["rg_margin"] if rg_spacing is None or az_spacing is None: az_spacing_inferred, rg_spacing_inferred = \ @@ -174,7 +179,11 @@ def ceil_divide(num: float, den: float) -> int: look_side=look_side, wavelength=wavelength, doppler=img_grid_doppler, - **radar_grid_params["bounding_box"], + min_height=min_height, + max_height=max_height, + pts_per_edge=pts_per_edge, + az_margin=az_margin, + rg_margin=rg_margin ) logger.info(f"Using radar grid: {radar_grid}") From 78a018dbd38d374fb7c8a75c5ad257cda045712a Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Mon, 26 Jan 2026 10:10:10 -0800 Subject: [PATCH 10/16] rename some radar grid parameters to match focus --- python/packages/nisar/workflows/static.py | 52 ++++++++++++++--------- share/nisar/defaults/static.yaml | 8 ++-- share/nisar/schemas/static.yaml | 8 ++-- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index 3c081dafb..77c59d722 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 @@ -111,10 +112,10 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: pts_per_side = radar_grid_spacing_params["pts_per_side"] bounding_box_params = radar_grid_params["bounding_box"] - sensing_start = bounding_box_params["sensing_start"] - sensing_end = bounding_box_params["sensing_end"] - starting_range = bounding_box_params["starting_range"] - ending_range = bounding_box_params["ending_range"] + start_time = bounding_box_params["start_time"] + end_time = bounding_box_params["end_time"] + start_range = bounding_box_params["start_range"] + end_range = bounding_box_params["end_range"] min_height = bounding_box_params["min_height"] max_height = bounding_box_params["max_height"] pts_per_edge = bounding_box_params["pts_per_edge"] @@ -137,34 +138,45 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: if az_spacing is None: az_spacing = az_spacing_inferred - if (sensing_start is not None and starting_range is not None and - sensing_end is not None and ending_range is not None): + if (start_time is not None and start_range is not None and + end_time is not None and end_range is not None): - # Use the provided bounding box parameters. - # Construct the radar grid. - # Divide `num` by `den`, rounded up to the next smallest integer. - def ceil_divide(num: float, den: float) -> int: - return int(np.ceil(num / den)) - num_az = ceil_divide(sensing_end - sensing_start, az_spacing) + 1 - num_rg = ceil_divide(ending_range - starting_range, rg_spacing) + 1 + epoch = orbit.reference_epoch + + t0 = (isce3.core.DateTime(start_time) - epoch).total_seconds() + tf = (isce3.core.DateTime(end_time) - epoch).total_seconds() + + num_az = round((tf - t0) / az_spacing) + num_rg = round((end_range - start_range) / rg_spacing) + + logger.info("Using user-specified radar grid bounding box parameters") + logger.info(f' start time: {start_time}') + logger.info(f' end time: {end_time}') + logger.info(f' start range: {start_range}') + logger.info(f' end range: {end_range}') + logger.info(f' az spacing: {az_spacing}') + logger.info(f' rg spacing: {rg_spacing}') + logger.info(f' number of lines: {num_az}') + logger.info(f' number of range samples: {num_rg}') radar_grid = isce3.product.RadarGridParameters( - sensing_start=sensing_start, + sensing_start=t0, wavelength=wavelength, prf=1.0 / az_spacing, - starting_range=starting_range, + starting_range=start_range, range_pixel_spacing=rg_spacing, lookside=normalize_look_side(look_side), length=num_az, width=num_rg, - ref_epoch=orbit.reference_epoch, + ref_epoch=epoch, ) - elif (sensing_start is not None or starting_range is not None or - sensing_end is not None or ending_range is not None): + + elif (start_time is not None or start_range is not None or + end_time is not None or end_range is not None): raise ValueError( "If specifying radar grid bounding box parameters, must provide" - " all of 'sensing_start', 'starting_range', 'sensing_end', and" - " 'ending_range'" + " all of 'start_time', 'start_range', 'end_time', and" + " 'end_range'" ) else: diff --git a/share/nisar/defaults/static.yaml b/share/nisar/defaults/static.yaml index 028e1e073..b59066874 100644 --- a/share/nisar/defaults/static.yaml +++ b/share/nisar/defaults/static.yaml @@ -247,24 +247,24 @@ runconfig: # 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. - sensing_start: + start_time: # [OPTIONAL] Azimuth ending time, in seconds since the epoch, of the last row of # the radar grid. # 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. - sensing_end: + end_time: # [OPTIONAL] Starting range, in meters, of the first column of the radar grid. # If not provided, it will be inferred from the specified geographic grid. # If provided, must be a positive value. - starting_range: + start_range: # [OPTIONAL] Ending range, in meters, of the last column of the radar grid. # If not provided, it will be inferred from the specified geographic grid. # If provided, must be a positive value. - ending_range: + end_range: # [OPTIONAL] Lower bound on the height of targets within the region of interest, # in meters above the reference ellipsoid of the output product. diff --git a/share/nisar/schemas/static.yaml b/share/nisar/schemas/static.yaml index 08401be1d..30baebdc9 100644 --- a/share/nisar/schemas/static.yaml +++ b/share/nisar/schemas/static.yaml @@ -99,10 +99,10 @@ radar_grid_spacing_options: pts_per_side: int(min=2, required=False) radar_grid_bounding_box_options: - sensing_start: num(required=False) - sensing_end: num(required=False) - starting_range: num(required=False) - ending_range: num(required=False) + start_time: timestamp() + end_time: timestamp() + start_range: num(required=False) + end_range: num(required=False) min_height: num(required=False) max_height: num(required=False) pts_per_edge: int(min=2, required=False) From 5948de697f2dce6eb64281097e1141e870f8ad58 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Mon, 26 Jan 2026 14:18:14 -0800 Subject: [PATCH 11/16] expose optional input RSLC into the STATIC workflow --- python/packages/nisar/static/ephemeris.py | 45 ++++++++++++++++++++--- python/packages/nisar/workflows/static.py | 34 +++++++++++++++++ share/nisar/defaults/static.yaml | 9 ++++- share/nisar/schemas/static.yaml | 19 +++++++--- 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/python/packages/nisar/static/ephemeris.py b/python/packages/nisar/static/ephemeris.py index 5fc249d83..01a48a6ec 100644 --- a/python/packages/nisar/static/ephemeris.py +++ b/python/packages/nisar/static/ephemeris.py @@ -4,7 +4,7 @@ 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 import isce3 @@ -13,6 +13,7 @@ def get_cropped_orbit_and_attitude( + input_file_path: str | os.PathLike, orbit_xml_file: str | os.PathLike, pointing_xml_file: str | os.PathLike, start_time: str | datetime | None, @@ -31,6 +32,8 @@ def get_cropped_orbit_and_attitude( Parameters ---------- + input_file_path : str | os.PathLike + Path to the input NISAR L1 RSLC formatted HDF5 file. orbit_xml_file : path-like Path to the input orbit ephemeris XML file. Must be an existing XML file conforming to the NISAR Orbit Ephemeris Product Specification\ [1]_. @@ -70,16 +73,46 @@ 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: + rslc_product = isce3.io.RSLCProduct(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 = isce3.io.RSLCProduct(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 = isce3.io.RSLCProduct(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/workflows/static.py b/python/packages/nisar/workflows/static.py index 77c59d722..3952c96c3 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -64,6 +64,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}") @@ -83,6 +84,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: # 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, orbit_xml_file=dynamic_ancillary_files["orbit_xml_file"], pointing_xml_file=dynamic_ancillary_files["pointing_xml_file"], **processing_params["ephemeris"], @@ -122,6 +124,38 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: az_margin = bounding_box_params["az_margin"] rg_margin = bounding_box_params["rg_margin"] + # load radar grid parameters from RSLC (if provided) + if input_file_path is not None: + logger.info("Load radar grid parameters from input RSLC file:") + rslc_product = isce3.io.RSLCProduct(str(input_file_path)) + rslc_radar_grid = rslc_product.getRadarGrid() + + 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 None: + start_time = (rslc_radar_grid.orbit.reference_epoch + + isce3.core.TimeDelta( + rslc_radar_grid.radar_grid.sensing_start)) + logger.info(f" start time: {start_time.isoformat()}") + + if end_time is None: + end_time = (rslc_radar_grid.orbit.reference_epoch + + isce3.core.TimeDelta( + rslc_radar_grid.radar_grid.sensing_stop)) + logger.info(f" end time: {end_time.isoformat()}") + + if start_range is None: + start_range = rslc_radar_grid.radar_grid.starting_range + logger.info(f" start range: {start_range}") + if end_range is None: + end_range = rslc_radar_grid.radar_grid.ending_range + logger.info(f" end range: {end_range}") + 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( diff --git a/share/nisar/defaults/static.yaml b/share/nisar/defaults/static.yaml index b59066874..f7235dfe2 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. diff --git a/share/nisar/schemas/static.yaml b/share/nisar/schemas/static.yaml index 30baebdc9..e3dfeba7b 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() 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) @@ -99,8 +100,16 @@ radar_grid_spacing_options: pts_per_side: int(min=2, required=False) radar_grid_bounding_box_options: - start_time: timestamp() - end_time: timestamp() + start_time: any( + timestamp(), + regex(r'^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}:\d{2})?$'), + required=False + ) + end_time: any( + timestamp(), + regex(r'^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}:\d{2})?$'), + required=False + ) start_range: num(required=False) end_range: num(required=False) min_height: num(required=False) From 98b8c7639e14ec4e0ce88df96d33d130565a4236 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Thu, 29 Jan 2026 14:11:20 -0800 Subject: [PATCH 12/16] enable use of an optional RSLC product to generate static layers --- cxx/isce3/geometry/loadDem.h | 2 +- .../pybind_isce3/geometry/DEMInterpolator.cpp | 81 ++++++++++ .../pybind_isce3/geometry/DEMInterpolator.h | 1 + .../pybind_isce3/geometry/geometry.cpp | 1 + python/packages/nisar/static/ephemeris.py | 7 +- python/packages/nisar/static/product.py | 9 +- python/packages/nisar/workflows/static.py | 138 ++++++++++++------ share/nisar/defaults/static.yaml | 4 + share/nisar/schemas/static.yaml | 6 +- 9 files changed, 197 insertions(+), 52 deletions(-) diff --git a/cxx/isce3/geometry/loadDem.h b/cxx/isce3/geometry/loadDem.h index d4c7ae57d..eb35139c8 100644 --- a/cxx/isce3/geometry/loadDem.h +++ b/cxx/isce3/geometry/loadDem.h @@ -61,7 +61,7 @@ isce3::geometry::DEMInterpolator DEMRasterToInterpolator( * (when the DEM is in geographic coordinates and `proj` is in polar stereo) * @param[in] minY Minimum Y/northing position * @param[in] maxY Maximum Y/northing position -* @param[out] dem_interp_block DEM interpolation object +* @param[out] dem_interp DEM interpolation object * @param[in] proj Projection object (nullptr to use same * DEM projection) * @param[in] dem_margin_x_in_pixels DEM X/easting margin in pixels diff --git a/python/extensions/pybind_isce3/geometry/DEMInterpolator.cpp b/python/extensions/pybind_isce3/geometry/DEMInterpolator.cpp index a82cab5d4..a9cf51f98 100644 --- a/python/extensions/pybind_isce3/geometry/DEMInterpolator.cpp +++ b/python/extensions/pybind_isce3/geometry/DEMInterpolator.cpp @@ -218,3 +218,84 @@ void addbinding_DEM_raster2interpolator(py::module& m) )") ; } + + +void addbinding_load_dem_from_proj(py::module& m) +{ + m.def("load_dem_from_proj", + [](isce3::io::Raster &dem_raster, + const double x0, + const double xf, + const double minY, + const double maxY, + const isce3::core::dataInterpMethod dem_interp_method, + isce3::core::ProjectionBase* proj, + const int dem_margin_x_in_pixels, + const int dem_margin_y_in_pixels, + const int dem_raster_band) { + + DEMInterp dem_interp(0, dem_interp_method); + + isce3::geometry::loadDemFromProj(dem_raster, + x0, + xf, + minY, + maxY, + &dem_interp, + proj, + dem_margin_x_in_pixels, + dem_margin_y_in_pixels, + dem_raster_band); + + return dem_interp; + }, + py::arg("dem_raster"), + py::arg("x0"), + py::arg("xf"), + py::arg("min_y"), + py::arg("max_y"), + py::arg("dem_interp_method") = isce3::core::BIQUINTIC_METHOD, + py::arg("proj") = nullptr, + py::arg("dem_margin_x_in_pixels") = 100, + py::arg("dem_margin_y_in_pixels") = 200, + py::arg("dem_raster_band") = 1, + R"( + Load DEM raster into a DEMInterpolator object around a given bounding box + in the same or different coordinate system as the DEM raster + Parameters + ---------- + dem_raster: isce3.io.Raster + Raster of the DEM + x0: double + If the DEM is in geographic coordinates and the `x0` coordinate is not + from the polar stereo system EPSG 3031 or EPSG 3413, this point represents + the minimum X coordinate value. In this case, the maximum + longitude span that this function can handle is 180 degrees + (when the DEM is in geographic coordinates and `proj` is in polar stereo + xf: double + Easting/longitude of eastern edge of bounding box + If the DEM is in geographic coordinates and the `xf` coordinate is not + from the polar stereo system EPSG 3031 or EPSG 3413, this point represents + the maximum X coordinate value. In this case, the maximum + longitude span that this function can handle is 180 degrees + (when the DEM is in geographic coordinates and `proj` is in polar stereo) + min_y: double + Minimum Y/northing position + max_y: double + Maximum Y/northing position + dem_interp_method: isce3.core.DataInterpMethod + DEM interpolation method + proj: + Projection object (nullptr to use same DEM projection) + dem_margin_x_in_pixels, int + DEM X/easting margin in pixels + dem_margin_y_in_pixels, int + DEM Y/northing margin in pixels + dem_raster_band: int + DEM raster band (starting from 1) + Returns + ------- + dem_interp: isce3.geometry.DEMInterpolator + DEM interpolator for given DEM raster and geo grid. + )"); +} diff --git a/python/extensions/pybind_isce3/geometry/DEMInterpolator.h b/python/extensions/pybind_isce3/geometry/DEMInterpolator.h index f5ccd765a..1fb9efca9 100644 --- a/python/extensions/pybind_isce3/geometry/DEMInterpolator.h +++ b/python/extensions/pybind_isce3/geometry/DEMInterpolator.h @@ -5,3 +5,4 @@ void addbinding(pybind11::class_&); void addbinding_DEM_raster2interpolator(pybind11::module&); +void addbinding_load_dem_from_proj(pybind11::module&); diff --git a/python/extensions/pybind_isce3/geometry/geometry.cpp b/python/extensions/pybind_isce3/geometry/geometry.cpp index 8193aa3bd..822985ed4 100644 --- a/python/extensions/pybind_isce3/geometry/geometry.cpp +++ b/python/extensions/pybind_isce3/geometry/geometry.cpp @@ -72,4 +72,5 @@ void addsubmodule_geometry(py::module & m) addbinding_pnt_intersect(geometry); addbinding_look_inc_from_sr(geometry); addbinding_DEM_raster2interpolator(geometry); + addbinding_load_dem_from_proj(geometry); } diff --git a/python/packages/nisar/static/ephemeris.py b/python/packages/nisar/static/ephemeris.py index 01a48a6ec..7b745c3c2 100644 --- a/python/packages/nisar/static/ephemeris.py +++ b/python/packages/nisar/static/ephemeris.py @@ -5,6 +5,7 @@ from nisar.products.readers.attitude import load_attitude_from_xml from nisar.products.readers.orbit import load_orbit_from_xml, load_orbit +from nisar.products.readers import SLC import isce3 @@ -78,7 +79,7 @@ def get_cropped_orbit_and_attitude( logger.info(f"Load orbit data from file {orbit_xml_file}") if input_file_path is not None: - rslc_product = isce3.io.RSLCProduct(str(input_file_path)) + 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) @@ -88,7 +89,7 @@ def get_cropped_orbit_and_attitude( 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 = isce3.io.RSLCProduct(str(input_file_path)) + rslc_product = SLC(hdf5file=str(input_file_path)) orbit_full = rslc_product.getOrbit() else: raise ValueError( @@ -106,7 +107,7 @@ def get_cropped_orbit_and_attitude( 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 = isce3.io.RSLCProduct(str(input_file_path)) + rslc_product = SLC(hdf5file=str(input_file_path)) attitude_full = rslc_product.getAttitude() else: raise ValueError( 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 3952c96c3..6952ae5bd 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -32,8 +32,9 @@ from nisar.static.water_mask import binarize_and_reproject_water_mask import isce3 -from isce3.geometry import make_geo_grid_bounding_polygon -from isce3.core import normalize_look_side +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 @@ -72,14 +73,27 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: # Construct a DEM interpolator. dem_interp_method = processing_params["dem"]["interp_method"] - dem = isce3.geometry.DEMInterpolator(dem_raster) - dem.interp_method = dem_interp_method # Construct the output geocoded coordinate grid. geo_grid_params = processing_params["geo_grid"] 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) + # dem.interp_method = dem_interp_method + dem_interp = load_dem_from_proj( + dem_raster, + geo_grid.start_x, + geo_grid.end_x, + geo_grid.end_y, + geo_grid.start_y, + normalize_data_interp_method(dem_interp_method), + 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. @@ -102,7 +116,6 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: # 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"] @@ -124,43 +137,79 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: az_margin = bounding_box_params["az_margin"] rg_margin = bounding_box_params["rg_margin"] - # load radar grid parameters from RSLC (if provided) + # Load radar grid parameters from RSLC (if provided) if input_file_path is not None: logger.info("Load radar grid parameters from input RSLC file:") - rslc_product = isce3.io.RSLCProduct(str(input_file_path)) + rslc_product = SLC(hdf5file=str(input_file_path)) rslc_radar_grid = rslc_product.getRadarGrid() + rslc_orbit = rslc_product.getOrbit() - 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 None: - start_time = (rslc_radar_grid.orbit.reference_epoch + + start_time = (rslc_orbit.reference_epoch + isce3.core.TimeDelta( - rslc_radar_grid.radar_grid.sensing_start)) + rslc_radar_grid.sensing_start)) logger.info(f" start time: {start_time.isoformat()}") if end_time is None: - end_time = (rslc_radar_grid.orbit.reference_epoch + + end_time = (rslc_orbit.reference_epoch + isce3.core.TimeDelta( - rslc_radar_grid.radar_grid.sensing_stop)) + rslc_radar_grid.sensing_stop)) logger.info(f" end time: {end_time.isoformat()}") if start_range is None: - start_range = rslc_radar_grid.radar_grid.starting_range + start_range = rslc_radar_grid.starting_range logger.info(f" start range: {start_range}") if end_range is None: - end_range = rslc_radar_grid.radar_grid.ending_range + 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}") + + # Print user radar grid bounding box parameters, if provided. + if (start_time is not None or start_range is not None or + end_time is not None or end_range is not None): + + logger.info("Using user-specified radar grid bounding box parameters") + if start_time is not None: + logger.info(f' start time: {start_time}') + if az_margin != 0.0: + start_time -= isce3.core.TimeDelta(az_margin) + logger.info(f' adjusted for az margin {az_margin}:' + f' {start_time}') + if end_time is not None: + logger.info(f' end time: {end_time}') + if az_margin != 0.0: + end_time += isce3.core.TimeDelta(az_margin) + logger.info(f' adjusted for az margin {az_margin}:' + f' {end_time}') + if start_range is not None: + logger.info(f' start range: {start_range}') + if rg_margin != 0.0: + start_range -= rg_margin + logger.info(f' adjusted for rg margin {rg_margin}:' + f' {start_range}') + if end_range is not None: + logger.info(f' end range: {end_range}') + if rg_margin != 0.0: + end_range += rg_margin + logger.info(f' adjusted for rg margin {rg_margin}:' + f' {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}') + 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, + dem=dem_interp, orbit=orbit, doppler=img_grid_doppler, look_side=look_side, @@ -169,8 +218,10 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: ) if rg_spacing is None: rg_spacing = rg_spacing_inferred + logger.info(f' inferred range spacing: {rg_spacing}') if az_spacing is None: az_spacing = az_spacing_inferred + logger.info(f' inferred azimuth time interval: {az_spacing}') if (start_time is not None and start_range is not None and end_time is not None and end_range is not None): @@ -183,13 +234,6 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: num_az = round((tf - t0) / az_spacing) num_rg = round((end_range - start_range) / rg_spacing) - logger.info("Using user-specified radar grid bounding box parameters") - logger.info(f' start time: {start_time}') - logger.info(f' end time: {end_time}') - logger.info(f' start range: {start_range}') - logger.info(f' end range: {end_range}') - logger.info(f' az spacing: {az_spacing}') - logger.info(f' rg spacing: {rg_spacing}') logger.info(f' number of lines: {num_az}') logger.info(f' number of range samples: {num_rg}') @@ -239,7 +283,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: radar_grid=radar_grid, orbit=orbit, attitude=attitude, - dem=dem, + dem=dem_interp, **processing_params["doppler"], ) @@ -292,15 +336,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 @@ -410,7 +457,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: identification_group = \ instrument_group.create_group("identification") bounding_polygon = make_geo_grid_bounding_polygon(geo_grid, - dem=dem) + dem=dem_interp) populate_identification_group( identification_group=identification_group, product_spec=product_spec, @@ -431,11 +478,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 f7235dfe2..3a14b7385 100644 --- a/share/nisar/defaults/static.yaml +++ b/share/nisar/defaults/static.yaml @@ -438,6 +438,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 e3dfeba7b..a0ad8652d 100644 --- a/share/nisar/schemas/static.yaml +++ b/share/nisar/schemas/static.yaml @@ -1,7 +1,7 @@ runconfig: groups: dynamic_ancillary_file_group: - input_file_path: str() + input_file_path: str(required=False) dem_raster_file: str() water_mask_raster_file: str(required=False) orbit_xml_file: str(required=False) @@ -153,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) From 7cf5a1a8f625332037d1997ded72abd90e758537 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Thu, 29 Jan 2026 15:00:03 -0800 Subject: [PATCH 13/16] improve annotations --- python/packages/nisar/static/ephemeris.py | 8 +++++--- python/packages/nisar/workflows/static.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python/packages/nisar/static/ephemeris.py b/python/packages/nisar/static/ephemeris.py index 7b745c3c2..5838738bb 100644 --- a/python/packages/nisar/static/ephemeris.py +++ b/python/packages/nisar/static/ephemeris.py @@ -14,9 +14,9 @@ def get_cropped_orbit_and_attitude( - input_file_path: str | os.PathLike, - orbit_xml_file: str | os.PathLike, - pointing_xml_file: str | os.PathLike, + input_file_path: str | os.PathLike | None, + orbit_xml_file: str | os.PathLike | None, + pointing_xml_file: str | os.PathLike | None, start_time: str | datetime | None, end_time: str | datetime | None, *, @@ -79,6 +79,8 @@ def get_cropped_orbit_and_attitude( 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, diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index 6952ae5bd..8a259d5bd 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -98,7 +98,7 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: # 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=input_file_path, orbit_xml_file=dynamic_ancillary_files["orbit_xml_file"], pointing_xml_file=dynamic_ancillary_files["pointing_xml_file"], **processing_params["ephemeris"], From f171e2217d48a46a784b20753ae7b5d4682c153b Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Thu, 29 Jan 2026 15:50:46 -0800 Subject: [PATCH 14/16] add default parameter values to get_cropped_orbit_and_attitude() --- python/packages/nisar/static/ephemeris.py | 16 ++++++++-------- python/packages/nisar/workflows/static.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/python/packages/nisar/static/ephemeris.py b/python/packages/nisar/static/ephemeris.py index 5838738bb..ad6eb445e 100644 --- a/python/packages/nisar/static/ephemeris.py +++ b/python/packages/nisar/static/ephemeris.py @@ -14,11 +14,11 @@ def get_cropped_orbit_and_attitude( - input_file_path: str | os.PathLike | None, - orbit_xml_file: str | os.PathLike | None, - pointing_xml_file: str | os.PathLike | None, - 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]: @@ -33,12 +33,12 @@ def get_cropped_orbit_and_attitude( Parameters ---------- - input_file_path : str | os.PathLike + input_file_path : str or os.PathLike or None Path to the input NISAR L1 RSLC formatted HDF5 file. - orbit_xml_file : path-like + 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 diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index 8a259d5bd..d36269a48 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -94,9 +94,9 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: normalize_data_interp_method(dem_interp_method), 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"], From 97129a3edc6b4f29c3d613642fddcabb05dab7ed Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Sun, 8 Feb 2026 09:20:21 -0800 Subject: [PATCH 15/16] enable fractional precision for radargrid start and end times --- share/nisar/defaults/static.yaml | 9 +++++---- share/nisar/schemas/static.yaml | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/share/nisar/defaults/static.yaml b/share/nisar/defaults/static.yaml index 3a14b7385..25f1768bf 100644 --- a/share/nisar/defaults/static.yaml +++ b/share/nisar/defaults/static.yaml @@ -247,15 +247,16 @@ runconfig: # Defaults to 5. pts_per_side: 5 bounding_box: - # [OPTIONAL] Azimuth start time, in seconds since the epoch, of the first row of - # the radar grid. + # [OPTIONAL] Azimuth start UTC date and time of the start of the radar + # observation, as a string in ISO 8601 format with up to nanosecond precision. # 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. start_time: - # [OPTIONAL] Azimuth ending time, in seconds since the epoch, of the last row of - # the radar grid. + # [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`. # 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. diff --git a/share/nisar/schemas/static.yaml b/share/nisar/schemas/static.yaml index a0ad8652d..5f29c5674 100644 --- a/share/nisar/schemas/static.yaml +++ b/share/nisar/schemas/static.yaml @@ -102,12 +102,12 @@ radar_grid_spacing_options: radar_grid_bounding_box_options: start_time: any( timestamp(), - regex(r'^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}:\d{2})?$'), + regex(r'^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d{1,9})?$'), required=False ) end_time: any( timestamp(), - regex(r'^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}:\d{2})?$'), + regex(r'^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d{1,9})?$'), required=False ) start_range: num(required=False) From 3383442fee8d2003f5066fffe37bda9de63a8ed2 Mon Sep 17 00:00:00 2001 From: "Gustavo H. X. Shiroma" Date: Sun, 8 Feb 2026 11:04:17 -0800 Subject: [PATCH 16/16] convert start & end date-time string to isce3.core.DateTime before adding TimeDelta --- python/packages/nisar/workflows/static.py | 85 +++++++++++++---------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/python/packages/nisar/workflows/static.py b/python/packages/nisar/workflows/static.py index d36269a48..c340ba553 100644 --- a/python/packages/nisar/workflows/static.py +++ b/python/packages/nisar/workflows/static.py @@ -127,8 +127,8 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: pts_per_side = radar_grid_spacing_params["pts_per_side"] bounding_box_params = radar_grid_params["bounding_box"] - start_time = bounding_box_params["start_time"] - end_time = bounding_box_params["end_time"] + start_datetime_str = bounding_box_params["start_time"] + end_datetime_str = bounding_box_params["end_time"] start_range = bounding_box_params["start_range"] end_range = bounding_box_params["end_range"] min_height = bounding_box_params["min_height"] @@ -137,8 +137,38 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: az_margin = bounding_box_params["az_margin"] rg_margin = bounding_box_params["rg_margin"] + start_time = None + end_time = None + + # Print user radar grid bounding box parameters, if provided. + if (start_datetime_str is not None or start_range is not None or + end_datetime_str is not None or end_range is not None): + + logger.info("Using user-specified radar grid bounding box parameters") + if start_datetime_str is not None: + logger.info(f' start time: {start_datetime_str}') + start_time = isce3.core.DateTime(start_datetime_str) + + if end_datetime_str is not None: + logger.info(f' end time: {end_datetime_str}') + end_time = isce3.core.DateTime(end_datetime_str) + + if start_range is not None: + logger.info(f' start range: {start_range}') + + if end_range is not None: + 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: + 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() @@ -171,39 +201,22 @@ def run_static_layers_workflow(config_file: os.PathLike | str) -> None: az_spacing = 1.0 / rslc_radar_grid.prf logger.info(f" azimuth time interval: {az_spacing}") - # Print user radar grid bounding box parameters, if provided. - if (start_time is not None or start_range is not None or - end_time is not None or end_range is not None): - - logger.info("Using user-specified radar grid bounding box parameters") - if start_time is not None: - logger.info(f' start time: {start_time}') - if az_margin != 0.0: - start_time -= isce3.core.TimeDelta(az_margin) - logger.info(f' adjusted for az margin {az_margin}:' - f' {start_time}') - if end_time is not None: - logger.info(f' end time: {end_time}') - if az_margin != 0.0: - end_time += isce3.core.TimeDelta(az_margin) - logger.info(f' adjusted for az margin {az_margin}:' - f' {end_time}') - if start_range is not None: - logger.info(f' start range: {start_range}') - if rg_margin != 0.0: - start_range -= rg_margin - logger.info(f' adjusted for rg margin {rg_margin}:' - f' {start_range}') - if end_range is not None: - logger.info(f' end range: {end_range}') - if rg_margin != 0.0: - end_range += rg_margin - logger.info(f' adjusted for rg margin {rg_margin}:' - f' {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}') + 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}:' + f' {start_time}') + if end_time is not None and az_margin != 0.0: + end_time += isce3.core.TimeDelta(az_margin) + logger.info(f' end time (adjusted for az. margin) {az_margin}:' + f' {end_time}') + if start_range is not None and rg_margin != 0.0: + start_range -= rg_margin + logger.info(f' start range (adjusted for rg margin) {rg_margin}:' + f' {start_range}') + if end_range is not None and rg_margin != 0.0: + end_range += rg_margin + logger.info(f' end range (adjusted for rg margin) {rg_margin}:' + f' {end_range}') if rg_spacing is None or az_spacing is None: az_spacing_inferred, rg_spacing_inferred = \