def make_doppler(cfg: Struct, frequency='A'): log.info("Generating Doppler LUT from pointing") orbit = get_orbit(cfg) attitude = get_attitude(cfg) dem = get_dem(cfg) opt = cfg.processing.doppler az = np.radians(opt.azimuth_boresight_deg) rawfiles = cfg.InputFileGroup.InputFilePath raw = Raw(hdf5file=rawfiles[0]) side = raw.identification.lookDirection fc = raw.getCenterFrequency(frequency) wvl = isce.core.speed_of_light / fc epoch, t, r = get_total_grid(rawfiles, opt.spacing.azimuth, opt.spacing.range) t = convert_epoch(t, epoch, orbit.reference_epoch) dop = np.zeros((len(t), len(r))) for i, ti in enumerate(t): _, v = orbit.interpolate(ti) vi = np.linalg.norm(v) for j, rj in enumerate(r): sq = squint(ti, rj, orbit, attitude, side, angle=az, dem=dem, **vars(opt.rdr2geo)) dop[i,j] = squint_to_doppler(sq, wvl, vi) lut = LUT2d(np.asarray(r), t, dop, opt.interp_method, False) log.info(f"Constructed Doppler LUT for fc={fc} Hz.") return fc, lut
def get_attitude(cfg: Struct): log.info("Loading attitude") if cfg.DynamicAncillaryFileGroup.Pointing: log.warning("Ignoring input pointing file. Using L0B attitude.") rawfiles = cfg.InputFileGroup.InputFilePath if len(rawfiles) > 1: raise NotImplementedError("Can't concatenate attitude data.") raw = Raw(hdf5file=rawfiles[0]) return raw.getAttitude()
def get_orbit(cfg: Struct): log.info("Loading orbit") if cfg.DynamicAncillaryFileGroup.Orbit: log.warning("Ignoring input orbit file. Using L0B orbits.") rawfiles = cfg.InputFileGroup.InputFilePath if len(rawfiles) > 1: raise NotImplementedError("Can't concatenate orbit data.") raw = Raw(hdf5file=rawfiles[0]) return raw.getOrbit()
def computeBoundingPolygon(h5file: str, h: float = 0.0): """Compute bounding polygon given (an otherwise complete) NISAR L0B product. Uses a fixed height in lieu of a digital elevation model. """ dem = isce3.geometry.DEMInterpolator(h) raw = Raw(hdf5file=h5file) orbit = raw.getOrbit() _, grid = raw.getRadarGrid() fc, doppler = make_doppler_lut([h5file], az=0.0) # Make sure we don't accidentally have an inconsistent wavelength. assert numpy.isclose(grid.wavelength, isce3.core.speed_of_light / fc) return isce3.geometry.get_geo_perimeter_wkt(grid, orbit, doppler, dem)
def get_total_grid_bounds(rawfiles: List[str], frequency='A'): times, ranges = [], [] for fn in rawfiles: raw = Raw(hdf5file=fn) pol = raw.polarizations[frequency][0] ranges.append(raw.getRanges(frequency)) times.append(raw.getPulseTimes(frequency, pol[0])) rmin = min(r[0] for r in ranges) rmax = max(r[-1] for r in ranges) dtmin = min(epoch + isce.core.TimeDelta(t[0]) for (epoch, t) in times) dtmax = max(epoch + isce.core.TimeDelta(t[-1]) for (epoch, t) in times) epoch = min(epoch for (epoch, t) in times) tmin = (dtmin - epoch).total_seconds() tmax = (dtmax - epoch).total_seconds() return epoch, tmin, tmax, rmin, rmax
def finalizeIdentification(h5file: str): """Add identification fields that depend on swath information: {"boundingPolygon"," zeroDopplerStartTime", "zeroDopplerEndTime"} """ # NOTE Need complete product to use product reader class, so assume we've # already populated dummy values. epoch, t = Raw(hdf5file=h5file).getPulseTimes() t0 = (epoch + isce3.core.TimeDelta(t[0])).isoformat() t1 = (epoch + isce3.core.TimeDelta(t[-1])).isoformat() poly = computeBoundingPolygon(h5file) with h5py.File(h5file, 'r+') as fid: ident = fid["/science/LSAR/identification"] # Unlink datasets and create new ones to avoid silent truncation. del ident["boundingPolygon"] del ident["zeroDopplerStartTime"] del ident["zeroDopplerEndTime"] def additem(name, value, description): ds = ident.create_dataset(name, data=numpy.string_(value)) ds.attrs["description"] = numpy.string_(description) additem( "boundingPolygon", poly, "OGR compatible WKT representation of bounding polygon of the image" ) additem("zeroDopplerStartTime", t0, "Azimuth start time of product") additem("zeroDopplerEndTime", t1, "Azimuth stop time of product")
def get_chirp(cfg: Struct, raw: Raw, frequency: str): if cfg.DynamicAncillaryFileGroup.Waveform: log.warning("Ignoring input waveform file. Using analytic chirp.") chirp = raw.getChirp(frequency) log.info(f"Chirp length = {len(chirp)}") window = get_window(cfg.processing.range_window, msg="Range window: ") chirp *= window(len(chirp)) log.info("Normalizing chirp to unit white noise gain.") return chirp / np.linalg.norm(chirp)**2
def focus(runconfig): # Strip off two leading namespaces. cfg = runconfig.runconfig.groups rawfiles = cfg.InputFileGroup.InputFilePath if len(rawfiles) <= 0: raise IOError("need at least one raw data file") if len(rawfiles) > 1: raise NotImplementedError("mixed-mode processing not yet supported") raw = Raw(hdf5file=rawfiles[0]) dem = get_dem(cfg) orbit = get_orbit(cfg) try: attitude = get_attitude(cfg) except: log.warning("Could not load attitude data. Assuming zero Doppler.") attitude = None # XXX This makes for zero-sized dimensions in the Doppler metadata. fc_ref, dop_ref = 1.0, isce.core.LUT2d() else: fc_ref, dop_ref = make_doppler(cfg) zerodop = isce.core.LUT2d() azres = cfg.processing.azcomp.azimuth_resolution atmos = cfg.processing.dry_troposphere_model or "nodelay" kernel = get_kernel(cfg) scale = cfg.processing.encoding_scale_factor use_gpu = check_gpu_opts(cfg) if use_gpu: # Set the current CUDA device. device = isce.cuda.core.Device(cfg.worker.gpu_id) isce.cuda.core.set_device(device) log.info(f"Processing using CUDA device {device.id} ({device.name})") backproject = isce.cuda.focus.backproject else: backproject = isce.focus.backproject # Generate reference output grid based on highest bandwidth, always A. log.info(f"Available polarizations: {raw.polarizations}") txref = raw.polarizations["A"][0][0] pulse_times, raw_grid = raw.getRadarGrid(frequency="A", tx=txref) log.info(f"len(pulses) = {len(pulse_times)}") log.info("Raw grid is %s", raw_grid) # Different grids for frequency A and B. ogrid = dict(A = make_output_grid(cfg, raw_grid)) log.info("Output grid A is %s", ogrid["A"]) if "B" in raw.frequencies: # Ensure aligned grids between A and B by just using an integer skip. # Sample rate of A is always an integer multiple of B. rskip = int(np.round(raw.getRanges("B").spacing / raw.getRanges("A").spacing)) ogrid["B"] = ogrid["A"][:, ::rskip] log.info("Output grid B is %s", ogrid["B"]) polygon = isce.geometry.get_geo_perimeter_wkt(ogrid["A"], orbit, zerodop, dem) fn = cfg.ProductPathGroup.SASOutputFile product = cfg.PrimaryExecutable.ProductType log.info(f"Creating output {product} product {fn}") slc = SLC(fn, mode="w", product=product) slc.set_orbit(orbit) # TODO acceleration, orbitType if attitude: slc.set_attitude(attitude, orbit.reference_epoch) slc.copy_identification(raw, polygon=polygon, track=cfg.Geometry.RelativeOrbitNumber, frame=cfg.Geometry.FrameNumber) # store metadata for each frequency dop = dict() for frequency in raw.frequencies: # TODO Find center frequencies after mode intersection. fc = raw.getCenterFrequency(frequency) dop[frequency] = scale_doppler(dop_ref, fc / fc_ref) slc.set_parameters(dop[frequency], orbit.reference_epoch, frequency) og = ogrid[frequency] t = og.sensing_start + np.arange(og.length) / og.prf r = og.starting_range + np.arange(og.width) * og.range_pixel_spacing slc.update_swath(t, og.ref_epoch, r, fc, frequency) # main processing loop channels = [(f, p) for f in raw.polarizations for p in raw.polarizations[f]] for frequency, pol in channels: log.info(f"Processing frequency{frequency} {pol}") rawdata = raw.getRawDataset(frequency, pol) log.info(f"Raw data shape = {rawdata.shape}") _, raw_grid = raw.getRadarGrid(frequency, tx=pol[0]) fc = raw.getCenterFrequency(frequency) na = cfg.processing.rangecomp.block_size.azimuth nr = rawdata.shape[1] log.info("Generating chirp") chirp = get_chirp(cfg, raw, frequency) rcmode = parse_rangecomp_mode(cfg.processing.rangecomp.mode) log.info(f"Preparing range compressor with {rcmode}") rc = isce.focus.RangeComp(chirp, nr, maxbatch=na, mode=rcmode) # Rangecomp modifies range grid. Also update wavelength. rc_grid = raw_grid.copy() rc_grid.starting_range -= ( rc_grid.range_pixel_spacing * rc.first_valid_sample) rc_grid.width = rc.output_size rc_grid.wavelength = isce.core.speed_of_light / fc igeom = isce.container.RadarGeometry(rc_grid, orbit, dop[frequency]) scratch = cfg.ProductPathGroup.ScratchPath fd = tempfile.NamedTemporaryFile(dir=scratch, suffix='.rc') log.info(f"Writing range compressed data to {fd.name}") rcfile = Raster(fd.name, rc.output_size, rawdata.shape[0], GDT_CFloat32) log.info(f"Range compressed data shape = {rcfile.data.shape}") for pulse in range(0, rawdata.shape[0], na): log.info(f"Range compressing block at pulse {pulse}") block = np.s_[pulse:pulse+na, :] # TODO fill invalid data during presum. ps = rawdata[block] ps[np.isnan(ps)] = 0.0 rc.rangecompress(rcfile.data[block], ps) acdata = slc.create_image(frequency, pol, shape=ogrid[frequency].shape) nr = cfg.processing.azcomp.block_size.range na = cfg.processing.azcomp.block_size.azimuth if nr < 1: nr = ogrid[frequency].width if not cfg.processing.is_enabled.azcomp: continue for i in range(0, ogrid[frequency].length, na): # h5py doesn't follow usual slice rules, raises exception # if dest_sel slices extend past dataset shape. imax = min(i + na, ogrid[frequency].length) for j in range(0, ogrid[frequency].width, nr): jmax = min(j + nr, ogrid[frequency].width) block = np.s_[i:imax, j:jmax] log.info(f"Azcomp block at (i, j) = ({i}, {j})") bgrid = ogrid[frequency][block] ogeom = isce.container.RadarGeometry(bgrid, orbit, zerodop) z = np.zeros(bgrid.shape, 'c8') backproject(z, ogeom, rcfile.data, igeom, dem, fc, azres, kernel, atmos, vars(cfg.processing.azcomp.rdr2geo), vars(cfg.processing.azcomp.geo2rdr)) log.debug(f"max(abs(z)) = {np.max(np.abs(z))}") zf = to_complex32(scale * z) acdata.write_direct(zf, dest_sel=block)
def make_doppler_lut(rawfiles: List[str], az: float = 0.0, orbit: isce.core.Orbit = None, attitude: isce.core.Attitude = None, dem: isce.geometry.DEMInterpolator = None, azimuth_spacing: float = 1.0, range_spacing: float = 1e3, frequency: str = "A", interp_method: str = "bilinear", **rdr2geo): """Generate Doppler look up table (LUT). Parameters ---------- rawfiles List of NISAR L0B format raw data files. az : optional Complement of the angle between the along-track axis of the antenna and its electrical boresight, in radians. Zero for non-scanned, flush- mounted antennas like ALOS-1. orbit : optional Path of antenna phase center. Defaults to orbit in first L0B file. attitude : optional Orientation of antenna. Defaults to attitude in first L0B file. dem : optional Digital elevation model, height in m above WGS84 ellipsoid. Default=0 m. azimuth_spacing : optional LUT grid spacing in azimuth, in seconds. Default=1 s. range_spacing : optional LUT grid spacing in range, in meters. Default=1000 m. frequency : {"A", "B"}, optional Band to use. Default="A" interp_method : optional LUT interpolation method. Default="bilinear". threshold : optional maxiter : optional extraiter : optional See rdr2geo Returns ------- fc Center frequency, in Hz, assumed for Doppler calculation. LUT Look up table of Doppler = f(r,t) """ # Input wrangling. assert len(rawfiles) > 0, "Need at least one L0B file." assert (azimuth_spacing > 0.0) and (range_spacing > 0.0) raw = Raw(hdf5file=rawfiles[0]) if orbit is None: orbit = raw.getOrbit() if attitude is None: attitude = raw.getAttitude() if dem is None: dem = isce.geometry.DEMInterpolator() # Assume look side and center frequency constant across files. side = raw.identification.lookDirection fc = raw.getCenterFrequency(frequency) # Now do the actual calculations. wvl = isce.core.speed_of_light / fc epoch, t, r = get_total_grid(rawfiles, azimuth_spacing, range_spacing) t = convert_epoch(t, epoch, orbit.reference_epoch) dop = np.zeros((len(t), len(r))) for i, ti in enumerate(t): _, v = orbit.interpolate(ti) vi = np.linalg.norm(v) for j, rj in enumerate(r): sq = squint(ti, rj, orbit, attitude, side, angle=az, dem=dem, **rdr2geo) dop[i, j] = squint_to_doppler(sq, wvl, vi) lut = LUT2d(np.asarray(r), t, dop, interp_method, False) return fc, lut