1
1
from .DataDecoder import DataDecoder
2
2
import h5py
3
+ import isce3
3
4
import logging
4
5
from pybind_nisar .products .readers import Base
5
6
import numpy as np
@@ -20,9 +21,10 @@ def find_case_insensitive(group: h5py.Group, name: str) -> str:
20
21
raise ValueError (f"{ name } not found in HDF5 group { group .name } " )
21
22
22
23
23
- class Raw (Base , family = 'nisar.productreader.raw' ):
24
+ class RawBase (Base , family = 'nisar.productreader.raw' ):
24
25
'''
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.
26
28
'''
27
29
productValidationType = pyre .properties .str (default = PRODUCT )
28
30
productValidationType .doc = 'Validation tag to ensure correct product type'
@@ -62,9 +64,19 @@ def parsePolarizations(self):
62
64
pols .append (t + r )
63
65
self .polarizations [freq ] = pols
64
66
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 ):
66
70
return f"{ self .SwathPath } /frequency{ frequency } "
67
71
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
+
68
80
def _rawGroup (self , frequency , polarization ):
69
81
tx , rx = polarization [0 ], polarization [1 ]
70
82
return f"{ self .BandPath (frequency )} /tx{ tx } /rx{ rx } "
@@ -81,21 +93,46 @@ def getRawDataset(self, frequency, polarization):
81
93
path = self .rawPath (frequency , polarization )
82
94
return DataDecoder (fid [path ])
83
95
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 .
86
98
"""
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 )
93
100
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
95
132
96
133
@property
97
134
def TelemetryPath (self ):
98
- return f"{ self .ProductPath } /telemetry "
135
+ return f"{ self .ProductPath } /lowRateTelemetry "
99
136
100
137
# XXX Base.getOrbit has @pyre.export decorator. What's that do?
101
138
# XXX L0B doesn't put orbit in MetadataPath
@@ -111,44 +148,62 @@ def getAttitude(self):
111
148
q = isce .core .Attitude .load_from_h5 (f [path ])
112
149
return q
113
150
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 )
116
153
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" ][()]
119
157
nr = len (r )
120
158
out = isce .core .Linspace (r [0 ], dr , nr )
121
159
assert np .isclose (out [- 1 ], r [- 1 ])
122
160
return out
123
161
124
162
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.
127
166
167
+ Parameters
168
+ ----------
169
+ frequency : {'A', 'B'}
170
+ Sub-band. Typically main science band is 'A'.
128
171
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
+ """
130
184
txpath = self .TransmitPath (frequency , tx )
131
185
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 " )
134
188
t = np .asarray (f [txpath ][name ])
135
189
epoch = isce .io .get_ref_epoch (f [txpath ], name )
136
190
return epoch , t
137
191
138
192
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 )
141
197
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" ][()]
144
199
145
200
146
201
# XXX C++ and Base.py assume SLC. Grid less well defined for Raw case
147
202
# since PRF isn't necessarily constant. Return pulse times with grid?
148
203
def getRadarGrid (self , frequency = 'A' , tx = 'H' , prf = None ):
149
- fc = self .getCenterFrequency (frequency )
204
+ fc = self .getCenterFrequency (frequency , tx )
150
205
wvl = isce .core .speed_of_light / fc
151
- r = self .getRanges (frequency )
206
+ r = self .getRanges (frequency , tx )
152
207
epoch , t = self .getPulseTimes (frequency , tx )
153
208
nt = len (t )
154
209
assert nt > 1
@@ -170,7 +225,6 @@ def getSubSwaths(self, frequency='A', tx='H'):
170
225
"""
171
226
txpath = self .TransmitPath (frequency , tx )
172
227
with h5py .File (self .filename , 'r' , libver = 'latest' , swmr = True ) as f :
173
- name = find_case_insensitive (f [txpath ], "UTCTime" )
174
228
ns = f [txpath ]["numberOfSubSwaths" ][()]
175
229
ss1 = f [txpath ]["validSamplesSubSwath1" ][:]
176
230
nt = ss1 .shape [0 ]
@@ -180,3 +234,96 @@ def getSubSwaths(self, frequency='A', tx='H'):
180
234
name = f"validSamplesSubSwath{ i + 1 } "
181
235
swaths [i , ...] = f [txpath ][name ][:]
182
236
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 )
0 commit comments