Skip to content

Commit b72742c

Browse files
bhawkinsbhawkins
authored andcommitted
Raw format (#804)
These patches intend to allow isce3 to work with either of the main flavors of the L0B format: the "old" format still in use by the ALOS reformatter and ReeUtilPy and the "new" format following the redesign by Piyush and Phil in 2020/2021. * Add unit test for raw data reader. * Add function to help open any flavor of NISAR L0B. * Update L0B spec and kludge old version. * Use smarter default for Raw.getCenterFrequency and prefer warning to warn. * Use updated Raw API in focus.py. * Add notes for possible attitude changes. * Implement attitude convention kludge. * Add notes to squint routine and fix right-looking case. * Simplify open_rrsd interface * Simplify attitude manipulations. * Expose chirp parameters. * almost_equal -> allclose * Clarify RawBase docstring. * Clarify seemingly unused keyword args * Fix docstring indent. * Prefer multiplication * Document getPulseTimes * Clarify s/c coordinates. * Use numpy docstring format for get_rcs2body. * Get specific about what "legacy" means. * Remove unnecessary keyword arguments. * Use tx= keyword to clarify code. Co-authored-by: bhawkins <[email protected]>
1 parent cb2be7c commit b72742c

File tree

5 files changed

+259
-46
lines changed

5 files changed

+259
-46
lines changed

python/packages/pybind_nisar/products/readers/Raw/Raw.py

Lines changed: 176 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .DataDecoder import DataDecoder
22
import h5py
3+
import isce3
34
import logging
45
from pybind_nisar.products.readers import Base
56
import numpy as np
@@ -20,9 +21,10 @@ def find_case_insensitive(group: h5py.Group, name: str) -> str:
2021
raise ValueError(f"{name} not found in HDF5 group {group.name}")
2122

2223

23-
class Raw(Base, family='nisar.productreader.raw'):
24+
class RawBase(Base, family='nisar.productreader.raw'):
2425
'''
25-
Class for parsing NISAR L0B products into isce structures.
26+
Base class for NISAR L0B products. Derived classes correspond to
27+
legacy (`LegacyRaw`) & current (`Raw`) versions of the product spec.
2628
'''
2729
productValidationType = pyre.properties.str(default=PRODUCT)
2830
productValidationType.doc = 'Validation tag to ensure correct product type'
@@ -62,9 +64,19 @@ def parsePolarizations(self):
6264
pols.append(t + r)
6365
self.polarizations[freq] = pols
6466

65-
def BandPath(self, frequency):
67+
# All methods assigned to _pulseMetaPath must present the same interface,
68+
# hence unused keyword arguments.
69+
def BandPath(self, frequency='A', **kw):
6670
return f"{self.SwathPath}/frequency{frequency}"
6771

72+
def TransmitPath(self, frequency='A', tx='H'):
73+
return f"{self.BandPath(frequency)}/tx{tx}"
74+
75+
# Some stuff got moved from BandPath to TransmitPath. This method allows
76+
# a way to override which one to use in subclasses. Intend to remove once
77+
# we're done transitioning raw data format.
78+
_pulseMetaPath = TransmitPath
79+
6880
def _rawGroup(self, frequency, polarization):
6981
tx, rx = polarization[0], polarization[1]
7082
return f"{self.BandPath(frequency)}/tx{tx}/rx{rx}"
@@ -81,21 +93,46 @@ def getRawDataset(self, frequency, polarization):
8193
path = self.rawPath(frequency, polarization)
8294
return DataDecoder(fid[path])
8395

84-
def getChirp(self, frequency: str = 'A'):
85-
"""Return analytic chirp for a given frequency.
96+
def getChirp(self, frequency: str = 'A', tx: str = 'H'):
97+
"""Return analytic chirp for a given band/transmit.
8698
"""
87-
with h5py.File(self.filename, 'r', libver='latest', swmr=True) as f:
88-
band = f[self.BandPath(frequency)]
89-
T = band["chirpDuration"][()]
90-
K = band["chirpSlope"][()]
91-
dr = band["slantRangeSpacing"][()]
92-
fs = isce.core.speed_of_light / 2 / dr
99+
_, fs, K, T = self.getChirpParameters(frequency, tx)
93100
log.info(f"Chirp({K}, {T}, {fs})")
94-
return np.asarray(isce.focus.form_linear_chirp(K, T, fs))
101+
return np.asarray(isce3.focus.form_linear_chirp(K, T, fs))
102+
103+
def getChirpParameters(self, frequency: str = 'A', tx: str = 'H'):
104+
"""Get metadata describing chirp.
105+
106+
Parameters
107+
----------
108+
frequency : {'A', 'B'}, optional
109+
Sub-band
110+
tx : {'H', 'V', 'L', 'R'}, optional
111+
Transmit polarization
112+
113+
Returns
114+
-------
115+
fc : float
116+
center frequency in Hz
117+
fs : float
118+
sample rate in Hz
119+
K : float
120+
chirp slope (signed) in Hz/s
121+
T : float
122+
chirp duration in s
123+
"""
124+
with h5py.File(self.filename, 'r', libver='latest', swmr=True) as f:
125+
group = f[self._pulseMetaPath(frequency=frequency, tx=tx)]
126+
T = group["chirpDuration"][()]
127+
K = group["chirpSlope"][()]
128+
dr = group["slantRangeSpacing"][()]
129+
fs = isce.core.speed_of_light / (2 * dr)
130+
fc = self.getCenterFrequency(frequency, tx)
131+
return fc, fs, K, T
95132

96133
@property
97134
def TelemetryPath(self):
98-
return f"{self.ProductPath}/telemetry"
135+
return f"{self.ProductPath}/lowRateTelemetry"
99136

100137
# XXX Base.getOrbit has @pyre.export decorator. What's that do?
101138
# XXX L0B doesn't put orbit in MetadataPath
@@ -111,44 +148,62 @@ def getAttitude(self):
111148
q = isce.core.Attitude.load_from_h5(f[path])
112149
return q
113150

114-
def getRanges(self, frequency='A'):
115-
bandpath = self.BandPath(frequency)
151+
def getRanges(self, frequency='A', tx='H'):
152+
path = self._pulseMetaPath(frequency=frequency, tx=tx)
116153
with h5py.File(self.filename, 'r', libver='latest', swmr=True) as f:
117-
r = np.asarray(f[bandpath]["slantRange"])
118-
dr = f[bandpath]["slantRangeSpacing"][()]
154+
group = f[path]
155+
r = np.asarray(group["slantRange"])
156+
dr = group["slantRangeSpacing"][()]
119157
nr = len(r)
120158
out = isce.core.Linspace(r[0], dr, nr)
121159
assert np.isclose(out[-1], r[-1])
122160
return out
123161

124162

125-
def TransmitPath(self, frequency='A', tx='H'):
126-
return f"{self.BandPath(frequency)}/tx{tx}"
163+
def getPulseTimes(self, frequency='A', tx='H'):
164+
"""
165+
Read pulse time tags.
127166
167+
Parameters
168+
----------
169+
frequency : {'A', 'B'}
170+
Sub-band. Typically main science band is 'A'.
128171
129-
def getPulseTimes(self, frequency='A', tx='H'):
172+
tx : {'H', 'V', 'L', 'R'}
173+
Transmit polarization. Abbreviations correspond to horizontal
174+
(linear), vertical (linear), left circular, right circular
175+
176+
Returns
177+
-------
178+
epoch : isce3.core.DateTime
179+
UTC time reference
180+
181+
t : array_like
182+
Transmit time of each pulse, in seconds relative to epoch.
183+
"""
130184
txpath = self.TransmitPath(frequency, tx)
131185
with h5py.File(self.filename, 'r', libver='latest', swmr=True) as f:
132-
# FIXME REE uses wrong case for UTCTime
133-
name = find_case_insensitive(f[txpath], "UTCTime")
186+
# FIXME product spec changed UTCTime -> UTCtime
187+
name = find_case_insensitive(f[txpath], "UTCtime")
134188
t = np.asarray(f[txpath][name])
135189
epoch = isce.io.get_ref_epoch(f[txpath], name)
136190
return epoch, t
137191

138192

139-
def getCenterFrequency(self, frequency: str = 'A'):
140-
bandpath = self.BandPath(frequency)
193+
def getCenterFrequency(self, frequency: str = 'A', tx: str = None):
194+
if tx is None:
195+
tx = self.polarizations[frequency][0][0]
196+
path = self._pulseMetaPath(frequency=frequency, tx=tx)
141197
with h5py.File(self.filename, 'r', libver='latest', swmr=True) as f:
142-
fc = f[bandpath]["centerFrequency"][()]
143-
return fc
198+
return f[path]["centerFrequency"][()]
144199

145200

146201
# XXX C++ and Base.py assume SLC. Grid less well defined for Raw case
147202
# since PRF isn't necessarily constant. Return pulse times with grid?
148203
def getRadarGrid(self, frequency='A', tx='H', prf=None):
149-
fc = self.getCenterFrequency(frequency)
204+
fc = self.getCenterFrequency(frequency, tx)
150205
wvl = isce.core.speed_of_light / fc
151-
r = self.getRanges(frequency)
206+
r = self.getRanges(frequency, tx)
152207
epoch, t = self.getPulseTimes(frequency, tx)
153208
nt = len(t)
154209
assert nt > 1
@@ -170,7 +225,6 @@ def getSubSwaths(self, frequency='A', tx='H'):
170225
"""
171226
txpath = self.TransmitPath(frequency, tx)
172227
with h5py.File(self.filename, 'r', libver='latest', swmr=True) as f:
173-
name = find_case_insensitive(f[txpath], "UTCTime")
174228
ns = f[txpath]["numberOfSubSwaths"][()]
175229
ss1 = f[txpath]["validSamplesSubSwath1"][:]
176230
nt = ss1.shape[0]
@@ -180,3 +234,96 @@ def getSubSwaths(self, frequency='A', tx='H'):
180234
name = f"validSamplesSubSwath{i+1}"
181235
swaths[i, ...] = f[txpath][name][:]
182236
return swaths
237+
238+
239+
# adapted from ReeUtilPy/REEout/AntPatAnalysis.py:getDCMant2sc
240+
def get_rcs2body(el_deg=37.0, az_deg=0.0, side='left') -> isce3.core.Quaternion:
241+
"""
242+
Get quaternion for conversion from antenna to spacecraft ijk, a forward-
243+
right-down body-fixed system. For details see section 8.1.2 of REE User's
244+
Guide (JPL D-95653).
245+
246+
Parameters
247+
----------
248+
el_deg : float
249+
angle (deg) between mounting X-Z plane and Antenna X-Z plane
250+
251+
az_deg : float
252+
angle (deg) between mounting Y-Z plane and Antenna Y-Z plane
253+
254+
side : {'right', 'left'}
255+
Radar look direction.
256+
257+
Returns
258+
-------
259+
q : isce3.core.Quaternion
260+
rcs-to-body quaternion
261+
"""
262+
d = -1.0 if side.lower() == 'left' else 1.0
263+
az, el = np.deg2rad([az_deg, el_deg])
264+
saz, caz = np.sin(az), np.cos(az)
265+
sel, cel = np.sin(el), np.cos(el)
266+
267+
R = np.array([
268+
[0, -d, 0],
269+
[d, 0, 0],
270+
[0, 0, 1]
271+
])
272+
Ry = np.array([
273+
[ cel, 0, sel],
274+
[ 0, 1, 0],
275+
[-sel, 0, cel]
276+
])
277+
Rx = np.array([
278+
[1, 0, 0],
279+
[0, caz, -saz],
280+
[0, saz, caz]
281+
])
282+
return isce3.core.Quaternion(R @ Ry @ Rx)
283+
284+
285+
class LegacyRaw(RawBase, family='nisar.productreader.raw'):
286+
"""
287+
Reader for legacy L0B format. Specicifally this corresponds to
288+
git commit ab2fcca of the PIX repository at
289+
https://github-fn.jpl.nasa.gov/NISAR-ADT/NISAR_PIX
290+
which occurred on 2019-09-09.
291+
"""
292+
def __init__(self, **kw):
293+
super().__init__(**kw)
294+
log.warning("Using deprecated L0B format.")
295+
# XXX Default configuration used in NISAR sims.
296+
self.rcs2body = get_rcs2body(side=self.identification.lookDirection)
297+
298+
@property
299+
def TelemetryPath(self):
300+
return f"{self.ProductPath}/telemetry"
301+
302+
_pulseMetaPath = RawBase.BandPath
303+
304+
def getAttitude(self):
305+
old = super().getAttitude()
306+
# XXX Big kludge: convert body2ecef to rcs2ecef.
307+
# Depends on self.rcs2body being set correctly.
308+
qs = [body2ecef * self.rcs2body for body2ecef in old.quaternions]
309+
return isce3.core.Attitude(old.time, qs, old.reference_epoch)
310+
311+
312+
313+
class Raw(RawBase, family='nisar.productreader.raw'):
314+
# TODO methods for new telemetry fields.
315+
pass
316+
317+
318+
def open_rrsd(filename) -> RawBase:
319+
"""Open a NISAR L0B file (RRSD product), returning a product reader of
320+
the appropriate type. Useful for supporting multiple variants of the
321+
evolving L0B product spec.
322+
"""
323+
# Peek at internal paths to try to determine flavor of L0B data.
324+
# A good check is the telemetry, which is split into high- and low-rate
325+
# groups in the 2020 updates.
326+
with h5py.File(filename, 'r', libver='latest', swmr=True) as f:
327+
if "/science/LSAR/RRSD/telemetry" in f:
328+
return LegacyRaw(hdf5file=filename)
329+
return Raw(hdf5file=filename)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .Raw import Raw
1+
from .Raw import Raw, open_rrsd
22
from .DataDecoder import complex32, DataDecoder

0 commit comments

Comments
 (0)