Skip to content

Commit a75e7f1

Browse files
authored
Merge branch 'isce-framework:develop' into develop
2 parents 22464ef + 7d15ed2 commit a75e7f1

File tree

11 files changed

+234
-101
lines changed

11 files changed

+234
-101
lines changed

python/packages/isce3/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
from .poly2d import fit_bivariate_polynomial
1717
from . import rdr_geo_block_generator
1818
from .block_param_generator import BlockParam
19+
from .projections import is_utm
1920
from .serialization import load_orbit_from_h5_group
2021
from . import types
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
def is_utm(epsg: int) -> bool:
2+
"""
3+
Check if the input EPSG code represents a UTM zone.
4+
5+
Parameters
6+
----------
7+
epsg : int
8+
The input EPSG code.
9+
10+
Returns
11+
-------
12+
bool
13+
True if the coordinate reference system represented by `epsg` is a Universal
14+
Transverse Mercator (UTM) projection; otherwise False.
15+
"""
16+
return (32600 < epsg <= 32660) or (32700 < epsg <= 32760)

python/packages/nisar/products/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
populate_global_attrs_from_spec,
1010
populate_dataset_attrs_from_spec,
1111
)
12+
from .projection import build_projection_dataset_attrs_dict

python/packages/nisar/products/insar/GOFF_writer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def add_parameters_to_procinfo_group(self):
6969
for rslc_name in ['reference', 'secondary']:
7070
rslc = self[self.group_paths.ParametersPath][rslc_name]
7171
rslc['referenceTerrainHeight'].attrs['description'] = \
72-
np.bytes_("Reference Terrain Height as a function of"
72+
np.bytes_("Reference terrain height as a function of"
7373
f" map coordinates for {rslc_name} RSLC")
7474
rslc['referenceTerrainHeight'].attrs['units'] = \
7575
Units.meter
@@ -124,7 +124,7 @@ def add_grids_to_hdf5(self):
124124
" where 1 is water and 0 is non-water;"
125125
" the second digit represents the subswath number of that pixel in the reference RSLC;"
126126
" the least-significant digit represents the subswath number of that pixel in the secondary RSLC."
127-
" A value of '0' in either subswath digit indicates an invalid sample in the corresponding RSLC"),
127+
" A value of 0 in either subswath digit indicates an invalid sample in the corresponding RSLC"),
128128
grid_mapping=grids_val,
129129
xds=xds,
130130
yds=yds,

python/packages/nisar/products/insar/GUNW_writer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def add_parameters_to_procinfo_group(self):
186186
for rslc_name in ['reference', 'secondary']:
187187
rslc = self[self.group_paths.ParametersPath][rslc_name]
188188
rslc['referenceTerrainHeight'].attrs['description'] = \
189-
np.bytes_("Reference Terrain Height as a function of"
189+
np.bytes_("Reference terrain height as a function of"
190190
f" map coordinates for {rslc_name} RSLC")
191191
rslc['referenceTerrainHeight'].attrs['units'] = \
192192
Units.meter
@@ -273,7 +273,7 @@ def add_grids_to_hdf5(self):
273273
" where 1 is water and 0 is non-water;"
274274
" the second digit represents the subswath number of that pixel in the reference RSLC;"
275275
" the least-significant digit represents the subswath number of that pixel in the secondary RSLC."
276-
" A value of '0' in either subswath digit indicates an invalid sample in the corresponding RSLC"),
276+
" A value of 0 in either subswath digit indicates an invalid sample in the corresponding RSLC"),
277277
grid_mapping=grids_val,
278278
xds=xds,
279279
yds=yds,

python/packages/nisar/products/insar/InSAR_L1_writer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def add_pixel_offsets_to_swaths_group(self):
502502
" subswath number of that pixel in the secondary RSLC,"
503503
" and the most significant digit represents"
504504
" the subswath number of that pixel in the reference RSLC."
505-
" A value of '0' in either digit indicates an invalid sample"
505+
" A value of 0 in either digit indicates an invalid sample"
506506
" in the corresponding RSLC"),
507507
fill_value=255)
508508
offset_group['mask'].attrs['long_name'] = np.bytes_("Valid samples subswath mask")

python/packages/nisar/products/insar/InSAR_base_writer.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any, Optional, Union
55

66
import h5py
7+
import journal
78
import numpy as np
89
from isce3.core import crop_external_orbit
910
from isce3.core.types import complex32, to_complex32
@@ -384,7 +385,7 @@ def add_RSLC_to_procinfo_params_group(self, rslc_name: str):
384385

385386
reference_terrain_height = "referenceTerrainHeight"
386387
reference_terrain_height_description = \
387-
f"Reference Terrain Height as a function of time for {rslc_name} RSLC"
388+
f"Reference terrain height as a function of time for {rslc_name} RSLC"
388389
if reference_terrain_height in src_param_group:
389390
src_param_group.copy(reference_terrain_height, dst_param_group)
390391
dst_param_group[reference_terrain_height].attrs['description'] = \
@@ -914,6 +915,7 @@ def add_identification_to_hdf5(self):
914915
"""
915916
Add the identification group to the product
916917
"""
918+
warning_channel = journal.warning('InSAR_base_writer.add_identification_to_hdf5')
917919
radar_band_name = self._get_band_name()
918920
primary_exec_cfg = self.cfg["primary_executable"]
919921

@@ -930,10 +932,13 @@ def add_identification_to_hdf5(self):
930932
processing_type = np.bytes_('Nominal')
931933
elif processing_type == 'UR':
932934
processing_type = np.bytes_('Urgent')
933-
elif processing_type == 'OD':
934-
processing_type = np.bytes_('Custom')
935935
else:
936-
processing_type = np.bytes_('Undefined')
936+
processing_type = np.bytes_('Custom')
937+
if processing_type != 'OD':
938+
warning_channel.log(
939+
'The processing type in the runconfig is set to'
940+
f' "{processing_type}", which is not a valid value'
941+
' for the output product metadata. Defaulting to "Custom"')
937942

938943
# Adopt same logic as RSLC, GSLC, GCOV
939944
# If no condition is met, assign string from runconfig
@@ -1162,7 +1167,10 @@ def add_identification_to_hdf5(self):
11621167
DatasetParams(
11631168
"processingType",
11641169
processing_type,
1165-
"Nominal (or) Urgent (or) Custom (or) Undefined",
1170+
'Processing pipeline used to generate this granule. ' \
1171+
'"Nominal": standard production system; "Urgent": time-sensitive ' \
1172+
'processing in response to urgent response events; ' \
1173+
'"Custom": user-initiated processing outside the nominal production system',
11661174
),
11671175
DatasetParams(
11681176
"radarBand", radar_band_name,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import numpy as np
2+
from osgeo import osr
3+
from numpy.typing import ArrayLike
4+
5+
import isce3
6+
from isce3.product.cf_conventions import get_grid_mapping_name
7+
8+
9+
def build_projection_dataset_attrs_dict(epsg: int) -> dict[str, ArrayLike]:
10+
"""
11+
Get attributes describing the spatial reference system of NISAR L2 products.
12+
13+
Returns a dict that may be used to populate the Attributes of `projection` Datasets
14+
in NISAR Level 2 products in a way that's compliant with Climate and Forecast (CF)
15+
Metadata Conventions.
16+
17+
Parameters
18+
----------
19+
epsg : int
20+
The EPSG code associated with the spatial reference system.
21+
22+
Returns
23+
-------
24+
dict
25+
A dict containing attributes describing the spatial reference system.
26+
String-valued attributes are represented by bytestrings with 'utf-8' encoding.
27+
"""
28+
srs = osr.SpatialReference()
29+
srs.ImportFromEPSG(epsg)
30+
31+
# Common attributes for all spatial reference systems.
32+
# FIXME: Should `spatial_ref` be replaced with `crs_wkt` for CF-1.7 compliance? See
33+
# https://github-fn.jpl.nasa.gov/NISAR-ADT/NISAR_PIX/issues/318.
34+
attrs = dict(
35+
epsg_code=epsg,
36+
spatial_ref=np.bytes_(srs.ExportToWkt()),
37+
grid_mapping_name=np.bytes_(get_grid_mapping_name(srs)),
38+
semi_major_axis=6378137.0,
39+
inverse_flattening=298.257223563,
40+
ellipsoid=np.bytes_("WGS84"),
41+
)
42+
43+
if epsg == 4326: # WGS 84
44+
attrs["longitude_of_prime_meridian"] = 0.0
45+
return attrs
46+
47+
# Common attributes for all projected spatial reference systems.
48+
attrs["false_easting"] = srs.GetProjParm(osr.SRS_PP_FALSE_EASTING)
49+
attrs["false_northing"] = srs.GetProjParm(osr.SRS_PP_FALSE_NORTHING)
50+
attrs["longitude_of_projection_origin"] = srs.GetProjParm(
51+
osr.SRS_PP_LONGITUDE_OF_ORIGIN
52+
)
53+
54+
if epsg == 3413: # Polar Stereographic (North)
55+
attrs["latitude_of_projection_origin"] = 90.0
56+
attrs["standard_parallel"] = 70.0
57+
attrs["straight_vertical_longitude_from_pole"] = -45.0
58+
return attrs
59+
60+
if epsg == 3031: # Polar Stereographic (South)
61+
attrs["latitude_of_projection_origin"] = -90.0
62+
attrs["standard_parallel"] = -71.0
63+
attrs["straight_vertical_longitude_from_pole"] = 0.0
64+
return attrs
65+
66+
# Attribute for non-Polar-Stereographic spatial reference systems.
67+
attrs["latitude_of_projection_origin"] = srs.GetProjParm(
68+
osr.SRS_PP_LATITUDE_OF_ORIGIN
69+
)
70+
71+
if isce3.core.is_utm(epsg):
72+
attrs["utm_zone_number"] = epsg % 100
73+
attrs["longitude_of_central_meridian"] = srs.GetProjParm(
74+
osr.SRS_PP_CENTRAL_MERIDIAN
75+
)
76+
attrs["scale_factor_at_central_meridian"] = srs.GetProjParm(
77+
osr.SRS_PP_SCALE_FACTOR
78+
)
79+
return attrs
80+
81+
if epsg == 6933: # EASE-Grid 2.0
82+
attrs["longitude_of_central_meridian"] = 0.0
83+
attrs["standard_parallel"] = 30.0
84+
return attrs
85+
86+
# FIXME: This CRS uses the GRS80 reference ellipsoid -- not WGS 84. The ellipsoid
87+
# parameters defined above are not valid for this EPSG code. LAEA Europe is not
88+
# supported anywhere else in ISCE3. Should we remove this? See
89+
# https://github.com/isce-framework/isce3/issues/72.
90+
if epsg == 3035: # LAEA Europe
91+
attrs["standard_parallel"] = -71.0
92+
attrs["straight_vertical_longitude_from_pole"] = 0.0
93+
return attrs
94+
95+
raise NotImplementedError(
96+
f"EPSG {epsg} waiting for implementation / not supported in ISCE3"
97+
)

python/packages/nisar/workflows/h5_prep.py

Lines changed: 17 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77

88
import h5py
99
import isce3
10-
from isce3.product.cf_conventions import get_grid_mapping_name
1110
import journal
1211
import numpy as np
1312
from isce3.core.types import complex32, to_complex32
1413
from isce3.io import HDF5OptimizedReader, optimize_chunk_size
1514
from nisar.h5 import cp_h5_meta_data
15+
from nisar.products.projection import build_projection_dataset_attrs_dict
1616
from nisar.products.readers import SLC
17-
from osgeo import osr
1817

1918

2019
def get_dataset_output_options(cfg: dict):
@@ -769,100 +768,19 @@ def set_get_geo_info(hdf5_obj, root_ds, geo_grid, z_vect=None,
769768
shape=(),
770769
dtype=np.uint32,
771770
data=epsg_code)
772-
# Set up osr for wkt
773-
srs = osr.SpatialReference()
774-
srs.ImportFromEPSG(epsg_code)
771+
772+
projds_attrs = build_projection_dataset_attrs_dict(epsg_code)
773+
projds.attrs.update(projds_attrs)
775774

776775
# Add projection description
777776
projds.attrs['description'] = np.bytes_('Product map grid projection: EPSG code, '
778777
'with additional projection information as HDF5 Attributes')
779778

780-
# WGS84 ellipsoid
781-
projds.attrs['semi_major_axis'] = 6378137.0
782-
projds.attrs['inverse_flattening'] = 298.257223563
783-
projds.attrs['ellipsoid'] = np.bytes_("WGS84")
784-
785-
# Additional fields
786-
projds.attrs['epsg_code'] = epsg_code
787-
788-
# CF 1.7+ requires this attribute to be named "crs_wkt"
789-
# spatial_ref is old GDAL way. Using that for testing only.
790-
# For NISAR replace with "crs_wkt"
791-
projds.attrs['spatial_ref'] = np.bytes_(srs.ExportToWkt())
792-
793-
# Here we have handcoded the attributes for the different cases
794-
# Recommended method is to use pyproj.CRS.to_cf() as shown above
795-
# To get complete set of attributes.
796-
797-
sr = osr.SpatialReference()
798-
sr.ImportFromEPSG(epsg_code)
799-
800-
projds.attrs['grid_mapping_name'] = np.bytes_(get_grid_mapping_name(sr))
801-
802-
# Set up units
803-
# Geodetic latitude / longitude
804-
if epsg_code == 4326:
805-
# Set up grid mapping
806-
projds.attrs['longitude_of_prime_meridian'] = 0.0
807-
else:
808-
# UTM zones
809-
if ((epsg_code > 32600 and
810-
epsg_code < 32661) or
811-
(epsg_code > 32700 and
812-
epsg_code < 32761)):
813-
# Set up grid mapping
814-
projds.attrs['utm_zone_number'] = epsg_code % 100
815-
projds.attrs["longitude_of_central_meridian"] = srs.GetProjParm(
816-
osr.SRS_PP_CENTRAL_MERIDIAN)
817-
projds.attrs["scale_factor_at_central_meridian"] = srs.GetProjParm(
818-
osr.SRS_PP_SCALE_FACTOR)
819-
820-
# Polar Stereo North
821-
elif epsg_code == 3413:
822-
# Set up grid mapping
823-
projds.attrs['latitude_of_projection_origin'] = 90.0
824-
projds.attrs['standard_parallel'] = 70.0
825-
projds.attrs['straight_vertical_longitude_from_pole'] = -45.0
826-
827-
# Polar Stereo south
828-
elif epsg_code == 3031:
829-
# Set up grid mapping
830-
projds.attrs['latitude_of_projection_origin'] = -90.0
831-
projds.attrs['standard_parallel'] = -71.0
832-
projds.attrs['straight_vertical_longitude_from_pole'] = 0.0
833-
834-
# EASE 2 for soil moisture L3
835-
elif epsg_code == 6933:
836-
# Set up grid mapping
837-
projds.attrs['longitude_of_central_meridian'] = 0.0
838-
projds.attrs['standard_parallel'] = 30.0
839-
840-
# Europe Equal Area for Deformation map (to be implemented in isce3)
841-
elif epsg_code == 3035:
842-
# Set up grid mapping
843-
projds.attrs['standard_parallel'] = -71.0
844-
projds.attrs['straight_vertical_longitude_from_pole'] = 0.0
845-
846-
else:
847-
raise NotImplementedError(
848-
f'EPSG {epsg_code} waiting for implementation / not supported in ISCE3')
849-
779+
if epsg_code != 4326:
850780
# Setup common parameters
851781
xds.attrs['long_name'] = np.bytes_("X coordinate of projection")
852782
yds.attrs['long_name'] = np.bytes_("Y coordinate of projection")
853783

854-
projds.attrs['false_easting'] = sr.GetProjParm(osr.SRS_PP_FALSE_EASTING)
855-
projds.attrs['false_northing'] = sr.GetProjParm(
856-
osr.SRS_PP_FALSE_NORTHING)
857-
858-
projds.attrs['longitude_of_projection_origin'] = sr.GetProjParm(
859-
osr.SRS_PP_LONGITUDE_OF_ORIGIN)
860-
861-
if epsg_code not in [3413, 3031]:
862-
projds.attrs['latitude_of_projection_origin'] = sr.GetProjParm(
863-
osr.SRS_PP_LATITUDE_OF_ORIGIN)
864-
865-
866784
if z_vect is not None:
867785
return zds, yds, xds
868786
return yds, xds
@@ -924,25 +842,33 @@ def add_radar_grid_cubes_to_hdf5(hdf5_obj, cube_group_name, geogrid,
924842
cube_group, 'losUnitVectorX', np.float32, cube_shape,
925843
zds=zds, yds=yds, xds=xds,
926844
long_name='LOS unit vector X',
927-
descr='East component of unit vector of LOS from target to sensor',
845+
descr='East component of the line-of-sight (LOS) unit vector, defined from ' \
846+
'the target to the sensor, expressed in the east-north-up (ENU) coordinate ' \
847+
'system with its origin at the target location',
928848
units='1', valid_min=-1.0, valid_max=1.0, **create_dataset_kwargs)
929849
los_unit_vector_y_raster = _get_raster_from_hdf5_ds(
930850
cube_group, 'losUnitVectorY', np.float32, cube_shape,
931851
zds=zds, yds=yds, xds=xds,
932852
long_name='LOS unit vector Y',
933-
descr='North component of unit vector of LOS from target to sensor',
853+
descr='North component of the line-of-sight (LOS) unit vector, defined from ' \
854+
'the target to the sensor, expressed in the east-north-up (ENU) coordinate ' \
855+
'system with its origin at the target location',
934856
units='1', valid_min=-1.0, valid_max=1.0, **create_dataset_kwargs)
935857
along_track_unit_vector_x_raster = _get_raster_from_hdf5_ds(
936858
cube_group, 'alongTrackUnitVectorX', np.float32, cube_shape,
937859
zds=zds, yds=yds, xds=xds,
938860
long_name='Along-track unit vector X',
939-
descr='East component of unit vector along ground track',
861+
descr='East component of the along-track unit vector at the target location, ' \
862+
'expressed in the east-north-up (ENU) coordinate system and projected ' \
863+
'onto the horizontal plane (i.e., excluding the up component)',
940864
units='1', valid_min=-1.0, valid_max=1.0, **create_dataset_kwargs)
941865
along_track_unit_vector_y_raster = _get_raster_from_hdf5_ds(
942866
cube_group, 'alongTrackUnitVectorY', np.float32, cube_shape,
943867
zds=zds, yds=yds, xds=xds,
944868
long_name='Along-track unit vector Y',
945-
descr='North component of unit vector along ground track',
869+
descr='North component of the along-track unit vector at the target location, ' \
870+
'expressed in the east-north-up (ENU) coordinate system and projected ' \
871+
'onto the horizontal plane (i.e., excluding the up component)',
946872
units='1', valid_min=-1.0, valid_max=1.0, **create_dataset_kwargs)
947873
elevation_angle_raster = _get_raster_from_hdf5_ds(
948874
cube_group, 'elevationAngle', np.float32, cube_shape,

tests/python/packages/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nisar/products/readers/raw.py
4646
nisar/products/readers/rslc.py
4747
nisar/products/readers/rslc_cal.py
4848
nisar/products/product_spec.py
49+
nisar/products/projection.py
4950
nisar/workflows/crossmul.py
5051
nisar/workflows/estimate_abscal_factor.py
5152
nisar/workflows/faraday_rot_angle_from_rslc.py

0 commit comments

Comments
 (0)