Skip to content

Commit 9920218

Browse files
vbrancatrad-eng-59bhawkins
authored andcommitted
Fix alos_to_nisar_l0b script (#1026)
* Expose offsets processing options * Generate standalone ROFF product * Include offsets product computation in InSAR workflow * Revert "Expose offsets processing options" This reverts commit e3ce076edde8eaf0cba2c3a19b291380224c7ea4. * Revert "Generate standalone ROFF product" This reverts commit 96b46d8e00362a9ed00ed5541adbc704a49a144d. * Revert "Include offsets product computation in InSAR workflow" This reverts commit ba7986d5f5f2c0805fe66b00cd720d2543c06fd9. * Correction to avoid unit tests to fail * Update L0B HDF5 structure * Add Hirad's patches: * Add a new cmd-line arg "mechanical_boresight" with default value 34.3 deg (beam number # 7 which is apparently the most common beam). This will allow users to set off-nadir angle needed for conversion from antenna to spacecraft body coodinate system. It would be great to fetch this directly from the respective off-nadir angle or at least the beam number from the meta data of ALOS1 PALSAR L1.0 products but that requires to dig into isce3 CEOS parser. * Use "Raw.get_rcs2body" to adjust the attitude quaternions to be consistent with L0B spec, that is RCS2ECEF (antenna to XYZ rather than body to XYZ) * Add two missing fields "ListOfRxTRMs" and "ListofTxTRMs" all being set to "[1]" given single channel SAR. Not sure how scan SAR mode is being handled and how many beams are used but assuming the beamforming is done on RF side, it is considered a single channel. * Use metadata instead of CLI arg to get elevation pointing offset. * Remove trailing whitespace. * add listOfTxPolarizations * Allow quadpol data * Add dummy calibration data * Add ground spacing Co-authored-by: Hirad Ghaemi <[email protected]> Co-authored-by: bhawkins <[email protected]>
1 parent 568bb45 commit 9920218

File tree

3 files changed

+82
-22
lines changed

3 files changed

+82
-22
lines changed

python/packages/isce3/stripmap/readers/l0raw/ALOS/CEOS/LeaderFile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def parseDatasetSummary(self, fid):
9393
#Specific check for ALOS stripmap, normal observation mode
9494
assert(record.SensorIDAndMode[0:9] == 'ALOS -L ')
9595
assert(record.SensorIDAndMode[10] == "H")
96-
assert(record.SensorIDAndMode[12:14] == "60")
96+
assert(record.SensorIDAndMode[12:14] in ("60", "62")) # high res or polsar
9797
assert(record.SensorClockAngle == 90.0)
9898
assert(record.RangePulseCodeSpecifier == "LINEAR FM CHIRP")
9999
assert(record.QuantizationDescriptor == "UNIFORM I,Q")

share/nisar/defaults/insar.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,6 @@ runconfig:
259259
# Number of offset estimates to process in batch along azimuth
260260
windows_batch_azimuth: 1
261261

262-
offsets_product:
263-
enabled: False
264-
265262
rubbersheet:
266263
# Path to dense offsets outputs (offsets, snr, covariance).
267264
# Not required as InSAR workflow allows using intermediate outputs
@@ -315,6 +312,10 @@ runconfig:
315312
filter_size_range: 5
316313
filter_size_azimuth: 5
317314

315+
offsets_product:
316+
# Enable/disable offsets product generation
317+
enabled: False
318+
318319
fine_resample:
319320
# Flag to enable/disable fine resampling
320321
enabled: True

share/nisar/examples/alos_to_nisar_l0b.py

100755100644
Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
import datetime
44
import glob
55
import h5py
6+
import isce3
67
import numpy
78
import os
89
from isce3.stripmap.readers.l0raw.ALOS.CEOS import ImageFile, LeaderFile
9-
import isce3
10+
from nisar.antenna.antenna_pattern import CalPath
1011
from nisar.products.readers.Raw import Raw
12+
from nisar.products.readers.Raw.Raw import get_rcs2body
1113
from nisar.workflows.focus import make_doppler_lut
1214

1315
def cmdLineParse():
1416
'''
1517
Command line parser.
1618
'''
17-
parser = argparse.ArgumentParser(description="Package ALOS L0 stripmap data into NISAR L0B HDF5")
19+
parser = argparse.ArgumentParser(description="Package ALOS L0 stripmap data into NISAR L0B HDF5",
20+
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
1821
parser.add_argument('-i', '--indir', dest='indir', type=str,
1922
help="Folder containing one ALOS L0 module",
2023
required=True)
@@ -153,6 +156,15 @@ def getset_attitude(group: h5py.Group, ldr: LeaderFile.LeaderFile,
153156
days = [sv.DayOfYear for sv in ldr.attitude.statevectors]
154157
assert all(numpy.diff(days) >= 0), "Unexpected roll over in day of year."
155158

159+
# build quaternion for antenna to spacecraft body (RCS to Body) per
160+
# mechanical boresight angle (MB). Not sure if "NominalOffNadirAngle" is
161+
# defined in exactly the same way, but it must be close for ALOS since
162+
# it's steered to zero-Doppler.
163+
q_rcs2body = get_rcs2body(el_deg=ldr.summary.NominalOffNadirAngle,
164+
side='right')
165+
print(f"Using off-nadir angle {ldr.summary.NominalOffNadirAngle} degrees"
166+
f" for beam number {ldr.summary.AntennaBeamNumber}.")
167+
156168
times = [] # time stamps, seconds rel orbit epoch
157169
rpys = [] # (roll, pitch, yaw) Euler angle tuples, degrees
158170
qs = [] # (q0,q1,q2,q3) quaternion arrays
@@ -165,7 +177,7 @@ def getset_attitude(group: h5py.Group, ldr: LeaderFile.LeaderFile,
165177
rpys.append(rpy)
166178
# Use time reference as orbit.
167179
times.append((t - orbit.reference_epoch).total_seconds())
168-
q = alos_quaternion(t, rpy[::-1], orbit)
180+
q = alos_quaternion(t, rpy[::-1], orbit) * q_rcs2body
169181
qs.append([q.w, q.x, q.y, q.z])
170182

171183
# Write to HDF5. isce3.core.Quaternion.save_to_h5 doesn't really cut it,
@@ -237,13 +249,38 @@ def constructNISARHDF5(args, ldr):
237249
inps.create_dataset('l0aGranules', data=numpy.string_([os.path.basename(args.indir)]))
238250

239251
#Start populating telemetry
240-
orbit_group = rrsd.create_group('telemetry/orbit')
252+
orbit_group = rrsd.create_group('lowRateTelemetry/orbit')
241253
orbit = get_alos_orbit(ldr)
242254
set_h5_orbit(orbit_group, orbit)
243-
attitude_group = rrsd.create_group("telemetry/attitude")
255+
attitude_group = rrsd.create_group("lowRateTelemetry/attitude")
244256
getset_attitude(attitude_group, ldr, orbit)
245257

246258

259+
def makeDummyCalType(n, first_bcal=0, first_lcal=500, interval=1000):
260+
'''Create a numpy array suitable for populating calType field.
261+
'''
262+
dtype = h5py.enum_dtype(dict(CalPath.__members__), basetype="uint8")
263+
x = numpy.zeros(n, dtype=dtype)
264+
x[:] = CalPath.HPA
265+
x[first_bcal::interval] = CalPath.BYPASS
266+
x[first_lcal::interval] = CalPath.LNA
267+
return x
268+
269+
270+
def getNominalSpacing(prf, dr, orbit, look_angle, i=0):
271+
"""Return ground spacing along- and across-track
272+
"""
273+
pos, vel = orbit.position[i], orbit.velocity[i]
274+
vs = numpy.linalg.norm(vel)
275+
ell = isce3.core.Ellipsoid()
276+
lon, lat, h = ell.xyz_to_lon_lat(pos)
277+
hdg = isce3.geometry.heading(lon, lat, vel)
278+
a = ell.r_dir(hdg, lat)
279+
ds = vs / prf * a / (a + h)
280+
dg = dr / numpy.sin(look_angle)
281+
return ds, dg
282+
283+
247284
def addImagery(h5file, ldr, imgfile, pol):
248285
'''
249286
Populate swaths segment of HDF5 file.
@@ -270,24 +307,26 @@ def addImagery(h5file, ldr, imgfile, pol):
270307
nPixels = image.description.NumberOfBytesOfSARDataPerRecord // image.description.NumberOfSamplesPerDataGroup
271308
nLines = image.description.NumberOfSARDataRecords
272309

310+
# Figure out nominal ground spacing
311+
prf = firstrec.PRFInmHz / 1000./ (1 + (ldr.summary.NumberOfSARChannels == 4))
312+
look_angle = numpy.radians(ldr.summary.NominalOffNadirAngle)
313+
ds, dg = getNominalSpacing(prf, dr, get_alos_orbit(ldr), look_angle)
314+
print('ds, dg =', ds, dg)
273315

274316
freqA = '/science/LSAR/RRSD/swaths/frequencyA'
275-
276317
#If this is first pol being written, add common information as well
277318
if freqA not in fid:
278319
freqA = fid.create_group(freqA)
279-
freqA.create_dataset('centerFrequency', data=SOL / (ldr.summary.RadarWavelengthInm))
280-
freqA.create_dataset('rangeBandwidth', data=ldr.calibration.header.BandwidthInMHz * 1.0e6)
281-
freqA.create_dataset('chirpDuration', data=firstrec.ChirpLengthInns * 1.0e-9)
282-
freqA.create_dataset('chirpSlope', data=-((freqA['rangeBandwidth'][()])/(freqA['chirpDuration'][()])))
283-
freqA.create_dataset('nominalAcquisitionPRF', data=firstrec.PRFInmHz / 1000./ (1 + (ldr.summary.NumberOfSARChannels == 4)))
284-
freqA.create_dataset('slantRangeSpacing', data=dr)
285-
freqA.create_dataset('slantRange', data=r0 + numpy.arange(nPixels) * dr)
320+
freqA.create_dataset("listOfTxPolarizations", data=numpy.string_([txP]),
321+
maxshape=2)
286322
else:
287323
freqA = fid[freqA]
288324

289-
#Add bunch of assertions here if you want to be super sure that values are not different between pols
290-
325+
txPolList = freqA["listOfTxPolarizations"]
326+
if not numpy.string_(txP) in txPolList:
327+
assert len(txPolList) == 1
328+
txPolList.resize((2,))
329+
txPolList[1] = txP
291330

292331
##Now add in transmit specific information
293332
txgrpstr = '/science/LSAR/RRSD/swaths/frequencyA/tx{0}'.format(txP)
@@ -303,6 +342,20 @@ def addImagery(h5file, ldr, imgfile, pol):
303342
txgrp.create_dataset('radarTime', dtype='f8', shape=(nLines,))
304343
txgrp.create_dataset('rangeLineIndex', dtype='i8', shape=(nLines,))
305344
txgrp.create_dataset('validSamplesSubSwath1', dtype='i8', shape=(nLines,2))
345+
txgrp.create_dataset('centerFrequency', data=SOL / (ldr.summary.RadarWavelengthInm))
346+
txgrp.create_dataset('rangeBandwidth', data=ldr.calibration.header.BandwidthInMHz * 1.0e6)
347+
txgrp.create_dataset('chirpDuration', data=firstrec.ChirpLengthInns * 1.0e-9)
348+
txgrp.create_dataset('chirpSlope', data=-((txgrp['rangeBandwidth'][()])/(txgrp['chirpDuration'][()])))
349+
txgrp.create_dataset('nominalAcquisitionPRF', data=prf)
350+
txgrp.create_dataset('slantRangeSpacing', data=dr)
351+
txgrp.create_dataset('slantRange', data=r0 + numpy.arange(nPixels) * dr)
352+
txgrp.create_dataset('listOfTxTRMs', data=numpy.asarray([1], dtype='uint8'))
353+
txgrp.create_dataset('sceneCenterAlongTrackSpacing', data=ds)
354+
txgrp.create_dataset('sceneCenterGroundRangeSpacing', data=dg)
355+
# dummy calibration data
356+
txgrp.create_dataset('txPhase', data=numpy.zeros((nLines,1), dtype='f4'))
357+
txgrp.create_dataset('chirpCorrelator', data=numpy.ones((nLines,1,3), dtype='c8'))
358+
txgrp.create_dataset('calType', data=makeDummyCalType(nLines))
306359
else:
307360
txgrp = fid[txgrpstr]
308361

@@ -313,7 +366,14 @@ def addImagery(h5file, ldr, imgfile, pol):
313366
raise ValueError('Reparsing polarization {0}. Array already exists {1}'.format(pol, rximgstr))
314367

315368
print('Dimensions: {0}L x {1}P'.format(nLines, nPixels))
316-
fid.create_group(rximgstr)
369+
rxgrp = fid.create_group(rximgstr)
370+
371+
# Dummy cal data
372+
rxgrp.create_dataset('caltone', data=numpy.ones((nLines,1), dtype='c8'))
373+
rxgrp.create_dataset('attenuation', data=numpy.ones((nLines,1), dtype='f4'))
374+
rxgrp.create_dataset('TRMDataWindow', data=numpy.ones((nLines,1), dtype='u1'))
375+
# Create List of RX TRMs
376+
rxgrp.create_dataset('listOfRxTRMs', data=numpy.asarray([1], dtype='uint8'))
317377

318378
##Set up BFPQLUT
319379
assert firstrec.SARRawSignalData.dtype.itemsize <= 2
@@ -372,11 +432,10 @@ def addImagery(h5file, ldr, imgfile, pol):
372432
if linnum != nLines:
373433
rec = image.readNextLine()
374434

375-
376435
if firstInPol:
377436
#Adjust time records - ALOS provides this only to nearest millisec - not good enough
378437
tinp = txgrp['UTCtime'][:]
379-
prf = freqA['nominalAcquisitionPRF'][()]
438+
prf = txgrp['nominalAcquisitionPRF'][()]
380439
tarr = (tinp - tinp[0]) * 1000
381440
ref = numpy.arange(tinp.size) / prf
382441

0 commit comments

Comments
 (0)