Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/nisarqa/input_product_readers/igram_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ def _get_path_containing_freq_pol(self, freq: str, pol: str) -> str:
@contextmanager
def get_wrapped_igram(
self, freq: str, pol: str
) -> Iterator[nisarqa.RadarRaster | nisarqa.GeoRaster]:
) -> Iterator[nisarqa.RadarRasterWithStats | nisarqa.GeoRasterWithStats]:
"""
Get the complex-valued wrapped interferogram image *Raster.
Get the complex-valued wrapped interferogram image *RasterWithStats.

Parameters
----------
Expand All @@ -44,15 +44,23 @@ def get_wrapped_igram(

Yields
------
raster : RadarRaster or GeoRaster
raster : RadarRasterWithStats or GeoRasterWithStats
Generated *Raster for the requested dataset.

Notes
-----
As of Jan 2026 (Product Spec Version 1.4.0), the wrapped interferogram
layers do not contain min/max/mean/std statistics as attributes.
This means that the `stats` attribute in the yielded `raster` will
contain `None` values for each metric. However, should this change
in subsequent ISCE3 releases, these values be populated accordingly.
"""
parent_path = self._wrapped_group_path(freq=freq, pol=pol)
path = f"{parent_path}/wrappedInterferogram"

with h5py.File(self.filepath) as f:
yield self._get_raster_from_path(
h5_file=f, raster_path=path, parse_stats=False
h5_file=f, raster_path=path, parse_stats=True
)

@contextmanager
Expand Down
93 changes: 12 additions & 81 deletions src/nisarqa/processing/calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,17 +239,20 @@ def hz2mhz(arr: np.ndarray) -> np.ndarray:
return arr * 1.0e-06


def compute_and_save_basic_statistics(
raster: nisarqa.Raster,
def process_and_save_basic_statistics(
raster: nisarqa.RadarRasterWithStats | nisarqa.GeoRasterWithStats,
stats_h5: h5py.File,
params: nisarqa.ThresholdParamGroup,
) -> None:
"""
Compute and save min, max, mean, std, % nan, % zero, % fill, % inf to HDF5.
Save min, max, mean, std, % nan, % zero, % fill, % inf to HDF5.

This function copies the min, max, mean, std provided via the input *Raster,
and it computes the % nan, % zero, % fill, % inf.

Parameters
----------
raster : nisarqa.Raster
raster : nisarqa.RadarRasterWithStats or nisarqa.GeoRasterWithStats
Input Raster.
stats_h5 : h5py.File
The output file to save QA metrics to.
Expand All @@ -266,85 +269,13 @@ def compute_and_save_basic_statistics(
-----
If the fill value is set to None in the input *Raster, that field will
not be computed nor included in the STATS.h5 file.
"""
arr = raster.data
units = raster.units
grp_path = raster.stats_h5_group_path
fill_value = raster.fill_value

# Step 1: Compute min/max/mean/STD
# TODO: refactor aka redesign this code chunk.

def _compute_min_max_mean_std(
arr: np.ndarray, component: str | None
) -> None:
"""
Compute min, max, mean, and samples standard deviation; save to HDF5.

Parameters
----------
arr : numpy.ndarray
The input array to have statistics run on. Note: If `arr` has a
complex dtype, this function will need to access the `arr.real`
and `arr.imag` parts separately. Unfortunately, h5py.Dataset
instances do not allow access to these parts using that syntax,
so to be safe, please pass in a Numpy array.
component : str or None
One of "real", "imag", or None.
Per ISCE3 convention, for complex-valued data, the statistics
should be computed independently for the real component and
for the imaginary component of the data.
If the source dataset is real-valued, set this to None.
If the source dataset is complex-valued, set this to "real" for the
real-valued component's name and description, or set to "imag"
for the imaginary component's name and description.

Notes
-----
TODO: This is a clunky, kludgy function. When these statistics get
implemented for RSLC, GSLC, and GCOV after R4, the developer
should consider pulling this function out into a standalone function.
For expediency, for R4, all InSAR products will use this function,
so this information can live here.
"""
if (component is not None) and (component not in ("real", "imag")):
raise ValueError(
f"`{component=!r}, must be 'real', 'imag', or None."
)

# Fill all invalid pixels in array with NaN, to easily compute metrics
arr_copy = np.where(
(np.isfinite(arr) & (arr != fill_value)), arr, np.nan
)

# Compute min/max/mean/std of valid pixels
for key, func in [
("min", np.nanmin),
("max", np.nanmax),
("mean", np.nanmean),
("std", lambda x: np.nanstd(x, ddof=1)),
]:

name, descr = nisarqa.get_stats_name_descr(
stat=key, component=component
)

nisarqa.create_dataset_in_h5group(
h5_file=stats_h5,
grp_path=grp_path,
ds_name=name,
ds_data=func(arr_copy),
ds_units=units,
ds_description=descr,
)
If the input raster's stats attribute contains metrics with values
of `None`, those metrics will not appear in the output stats HDF5.
"""

if raster.is_complex:
# HDF5 Datasets cannot access .real nor .imag, so we need
# to read the array into a numpy array in memory first.
_compute_min_max_mean_std(arr[()].real, "real")
_compute_min_max_mean_std(arr[()].imag, "imag")
else:
_compute_min_max_mean_std(arr[()], None)
# Step 1: Copy min/max/mean/STD
nisarqa.copy_imagery_metrics_to_stats_h5(raster=raster, stats_h5=stats_h5)

# Step 2: Compute % NaN, % Inf, % Fill, % near-zero, % invalid
compute_percentage_metrics(
Expand Down
4 changes: 2 additions & 2 deletions src/nisarqa/processing/insar/az_and_slant_rng_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,11 @@ def process_range_and_az_offsets(
"""
# Compute Statistics first, in case of malformed layers
# (which could cause plotting to fail)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=az_offset, stats_h5=stats_h5, params=params
)

nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=rg_offset, stats_h5=stats_h5, params=params
)

Expand Down
4 changes: 2 additions & 2 deletions src/nisarqa/processing/insar/az_and_slant_rng_variances.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ def process_az_and_slant_rg_variances_from_offset_product(
):
# Compute Statistics first, in case of malformed layers
# (which could cause plotting to fail)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=az_off_var,
params=params,
stats_h5=stats_h5,
)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=rg_off_var,
params=params,
stats_h5=stats_h5,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def process_surface_peak(
) as surface_peak,
):
# Compute Statistics first, in case of malformed layers
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=surface_peak,
params=params_surface_peak,
stats_h5=stats_h5,
Expand Down Expand Up @@ -249,12 +249,12 @@ def process_cross_variance_and_surface_peak(
) as surface_peak,
):
# Compute Statistics first, in case of malformed layers
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=cross_off_var,
params=params_cross_offset,
stats_h5=stats_h5,
)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=surface_peak,
params=params_surface_peak,
stats_h5=stats_h5,
Expand Down
4 changes: 2 additions & 2 deletions src/nisarqa/processing/insar/ionosphere_phase_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ def process_ionosphere_phase_screen(
):
# Compute Statistics first, in case of malformed layers
# (which could cause plotting to fail)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=iono_phs,
stats_h5=stats_h5,
params=params_iono_phs_screen,
)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=iono_uncertainty,
stats_h5=stats_h5,
params=params_iono_phs_uncert,
Expand Down
2 changes: 1 addition & 1 deletion src/nisarqa/processing/insar/unwrapped_coh_mag.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def process_unw_coh_mag(
with product.get_unwrapped_coh_mag(freq=freq, pol=pol) as coh_mag:
# Compute Statistics first, in case of malformed layers
# (which could cause plotting to fail)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=coh_mag,
params=params,
stats_h5=stats_h5,
Expand Down
2 changes: 1 addition & 1 deletion src/nisarqa/processing/insar/unwrapped_phase_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def process_phase_image_unwrapped(
with product.get_unwrapped_phase(freq=freq, pol=pol) as img:
# Compute Statistics first, in case of malformed layers
# (which could cause plotting to fail)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=img, stats_h5=stats_h5, params=params
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ def process_phase_image_wrapped(
):
# Compute Statistics first, in case of malformed layers
# (which could cause plotting to fail)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=complex_img,
params=params_wrapped_igram,
stats_h5=stats_h5,
)
nisarqa.compute_and_save_basic_statistics(
nisarqa.process_and_save_basic_statistics(
raster=coh_img,
params=params_coh_mag,
stats_h5=stats_h5,
Expand Down
95 changes: 56 additions & 39 deletions src/nisarqa/processing/stats_h5_writer/metrics_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,65 @@ def get_stats_name_descr(stat: str, component: str | None) -> tuple[str, str]:
)


def copy_imagery_metrics_to_stats_h5(
raster: nisarqa.RadarRasterWithStats | nisarqa.GeoRasterWithStats,
stats_h5: h5py.File,
) -> None:
"""
Copy min, max, mean, and std from input raster to HDF5.

Parameters
----------
raster : nisarqa.RadarRasterWithStats or nisarqa.GeoRasterWithStats
Input Raster.
stats_h5 : h5py.File
The output file to save metrics to.

Notes
-----
If the input raster's stats attribute contains metrics with values
of `None`, those metrics will not appear in the output stats HDF5.
"""
for m in ("min", "max", "mean", "std"):
metrics = []
if raster.is_complex:
for component in ("real", "imag"):
# get tuple of (val, name, descr)
val_name_descr = raster.get_stat_val_name_descr(
stat=m, component=component
)
metrics.append(val_name_descr)
else:
# get tuple of (val, name, descr)
val_name_descr = raster.get_stat_val_name_descr(stat=m)
metrics.append(val_name_descr)

for val, name, descr in metrics:
if val is not None:
# XXX: This will create a dataset with a snake_case name,
# which differs from the usual camelCase naming convention,
# but matches the name of the attribute in the input
# product.
nisarqa.create_dataset_in_h5group(
h5_file=stats_h5,
grp_path=raster.stats_h5_group_path,
ds_name=name,
ds_data=val,
ds_description=descr,
ds_units=raster.units,
)
else:
nisarqa.get_logger().error(
f"Attribute `{name}` is missing or has no"
f" value. Dataset: {raster.name}"
)


def copy_non_insar_imagery_metrics(
product: nisarqa.NonInsarProduct, stats_h5: h5py.File
) -> None:
"""
Copy min/max/mean/std metrics of freq+pol imagery layers to QA HDF5.
Copy min/max/mean/std metrics of all freq+pol imagery layers to QA HDF5.

This function accommodates both real and complex datasets.

Expand All @@ -607,49 +661,12 @@ def copy_non_insar_imagery_metrics(
stats_h5 : h5py.File
Handle to an HDF5 file where the metrics should be saved.
"""
log = nisarqa.get_logger()

for freq in product.freqs:
for pol in product.get_pols(freq=freq):
with product.get_raster(freq=freq, pol=pol) as img:
dest_path = (
f"{nisarqa.STATS_H5_QA_FREQ_GROUP % (product.band, freq)}"
f"/{pol}"
)

for m in ("min", "max", "mean", "std"):
metrics = []
if img.is_complex:
for component in ("real", "imag"):
# get tuple of (val, name, descr)
val_name_descr = img.get_stat_val_name_descr(
stat=m, component=component
)
metrics.append(val_name_descr)
else:
# get tuple of (val, name, descr)
val_name_descr = img.get_stat_val_name_descr(stat=m)
metrics.append(val_name_descr)

for val, name, descr in metrics:
if val is not None:
# XXX: This will create a dataset with a snake_case name,
# which differs from the usual camelCase naming convention,
# but matches the name of the attribute in the input
# product.
nisarqa.create_dataset_in_h5group(
h5_file=stats_h5,
grp_path=dest_path,
ds_name=name,
ds_data=val,
ds_description=descr,
ds_units=img.units,
)
else:
log.error(
f"Attribute `{name}` is missing or has no"
f" value. Dataset: {img.name}"
)
copy_imagery_metrics_to_stats_h5(raster=img, stats_h5=stats_h5)


__all__ = nisarqa.get_all(__name__, objects_to_skip)