- Home
- Documentation
- API reference
- Readers
- hyperproc.readers.aviris
hyperproc.readers.aviris¶
Source-derived reference
Generated from the current hyperproc 0.1.2 checkout.
Implementation: hyperproc/readers/aviris.py. Signatures, defaults, docstrings, and expandable source are extracted statically; the module is not imported or executed. Names beginning with _ are implementation details, not a stable public API.
Use the function signature as the authority for individual parameter defaults and return annotations. Original docstrings sometimes group parameter names or wrap return descriptions across lines; these descriptions are preserved rather than inferred or rewritten.
AVIRIS reader - Classic, NG, 3 and 5, radiance and reflectance.
Four instruments in one JPL family, spanning thirty years and two file formats. All four ship orthorectified cubes, so unlike EMIT or PACE there is no GLT to apply to the image itself - the reader hands back a map-projected grid.
================ ========================== ===== ===========================
instrument granule id bands cube
================ ========================== ===== ===========================
AVIRIS-Classic f201013t01p00r10 224 *_sc01_ort_img,
*_corr_*_img (ENVI)
AVIRIS-NG ang20220224t210144 425 *_rdn_*_img,
*_rfl_*_img (ENVI)
AVIRIS-3 AV320231005t181518 284 *_RDN_ORT,
*_RFL_ORT (ENVI)
AVIRIS-5 AV520250508t173511_000 424 *_RDN.nc,
*_RFL_ORT.nc (NetCDF-4)
================ ========================== ===== ===========================
Six things to know:
-
The grids are flight-aligned, not north-up. Every ENVI variant carries a
rotationin itsmap info- -13 deg for the AVIRIS-3 line here, -50 for the NG one, +29 for Classic - so the affine has non-zero shear terms and the 1-Dx/ycoordinates cannot describe it on their own.attrs["transform"](GDAL order) is the authoritative georeferencing; passmap_coords=Truefor exact 2-Deasting/northing. AVIRIS-5 is the exception - it ships a north-up NetCDF grid. -
Classic radiance is scaled integers. The cube is big-endian
int16and must be divided by the per-band factors in the.gainsidecar (300, 600 or 1200 here) to reach uW nm-1 cm-2 sr-1. NG and AVIRIS-3 store float radiance in those units already. Reflectance is unitless 0-1 everywhere. -
Geometry lives in a separate file, often a separate directory. The OBS cube carries path length, view and solar angles, slope, aspect and
cos_i- everything topographic and BRDF correction needs. For NG and Classic the reflectance and radiance ship as two sibling folders and only the radiance one holds the OBS, so the reader searches sibling directories for the same granule id. -
AVIRIS-5's two levels are not on the same grid. Its L2A reflectance is orthorectified, but its OBS and its L1B radiance are raw
(line, sample)and ship their own lookup table, so the reader applies that GLT to bring them onto the reflectance grid. The other three deliver*_obs_ortand ortho cubes throughout. Note the radiance is a separate ORNL DAAC collection (AV5_L1B_RDN) from the L2A bundle, so a folder holding only L2A products genuinely has no radiance in it. -
AVIRIS-3's slope and cos_i are wrong as delivered. Slope is stored from vertical, and
cos_iis computed from that, which leaves both unusable for topographic correction. This is not assumed per instrument: every variant ships a DEM, so :func:check_geometrydifferentiates it and decides from the correlation, and :func:fix_slope_conventionrebuilds the pair when the verdict says so.fix_geometry=Falsereturns the file untouched. -
No-data is not -9999 everywhere. Classic radiance declares none in its header and fills off-swath cells with -50, which survives the gain division as -0.167 and drags a scene median negative if it is not masked.
Cubes here run 10-30 GB, so they are opened lazily through dask; nothing is
read until you slice or compute. wl_range subsets bands before any read.
FILL = -9999.0
module-attribute
¶
CLASSIC_RDN_FILL = -50.0
module-attribute
¶
WATER_BANDS = ((1340.0, 1440.0), (1800.0, 1975.0))
module-attribute
¶
OBS_BANDS = (('path length', 'path_length'), ('to-sensor azimuth', 'vaa'), ('to-sensor zenith', 'vza'), ('to-sun azimuth', 'saa'), ('to-sun zenith', 'sza'), ('phase', 'phase'), ('slope', 'slope'), ('aspect', 'aspect'), ('cosine', 'cos_i'), ('utc time', 'utc_time'), ('earth-sun distance', 'earth_sun_distance'))
module-attribute
¶
OBS_UNITS = {'path_length': ('m', 'sensor-to-ground path length'), 'vaa': ('degrees', 'to-sensor azimuth, cw from north'), 'vza': ('degrees', 'to-sensor zenith'), 'saa': ('degrees', 'to-sun azimuth, cw from north'), 'sza': ('degrees', 'to-sun zenith'), 'phase': ('degrees', 'solar phase angle'), 'slope': ('degrees', 'terrain slope'), 'aspect': ('degrees', 'terrain aspect, cw from north'), 'cos_i': ('1', 'cosine of the solar incidence angle on the slope'), 'utc_time': ('hours', 'UTC time of acquisition'), 'earth_sun_distance': ('AU', 'earth-sun distance')}
module-attribute
¶
AV5_OBS = {'path_length': 'path_length', 'to_sensor_azimuth': 'vaa', 'to_sensor_zenith': 'vza', 'to_sun_azimuth': 'saa', 'to_sun_zenith': 'sza', 'solar_phase': 'phase', 'slope': 'slope', 'aspect': 'aspect', 'cosine_i': 'cos_i', 'utc_time': 'utc_time', 'earth_sun_distance': 'earth_sun_distance'}
module-attribute
¶
ELEVATION = {'AVIRIS-3': (('*_LOC_ORT',), 3, ()), 'AVIRIS-NG': (('*_loc', '*_igm'), 3, ('*_glt',)), 'AVIRIS-Classic': (('*_ort_igm',), 3, ('*_ort_glt',))}
module-attribute
¶
TOPO_LAYERS = ('slope', 'aspect', 'cos_i', 'sza', 'saa', 'vza', 'vaa', 'raa', 'elev', 'path_length')
module-attribute
¶
VARIANTS = (_Variant('AVIRIS-3', re.compile('(AV3\\d{8}t\\d{6})'), rdn=('*_RDN_ORT',), rfl=('*_RFL_ORT',), obs=('*_OBS_ORT',), unc=('*_UNC_ORT',), atm=('*_ATM_ORT',)), _Variant('AVIRIS-5', re.compile('(AV5\\d{8}t\\d{6}_\\d{3})'), rdn=('*_L1B_RDN_*_RDN.nc',), rfl=('*_RFL_ORT.nc',), obs=('*_L1B_ORT_*_OBS.nc',), unc=('*_UNC_ORT.nc',)), _Variant('AVIRIS-NG', re.compile('(ang\\d{8}t\\d{6})'), rdn=('*_rdn_*_img',), rfl=('*_rfl_*_img',), obs=('*_obs_ort',)), _Variant('AVIRIS-Classic', re.compile('(f\\d{6}t\\d{2}p\\d{2}r\\d{2})'), rdn=('*_sc01_ort_img', '*rdn*_ort_img'), rfl=('*_corr_*_img', '*_refl_*_img'), obs=('*_obs_ort',), atm=('*_h2o_*_img',)))
module-attribute
¶
_Variant
dataclass
¶
How one instrument names its files.
Source code in hyperproc/readers/aviris.py
name: str
instance-attribute
¶
granule: re.Pattern
instance-attribute
¶
rdn: tuple[str, ...]
instance-attribute
¶
rfl: tuple[str, ...]
instance-attribute
¶
obs: tuple[str, ...]
instance-attribute
¶
unc: tuple[str, ...] = ()
class-attribute
instance-attribute
¶
atm: tuple[str, ...] = ()
class-attribute
instance-attribute
¶
__init__(name: str, granule: re.Pattern, rdn: tuple[str, ...], rfl: tuple[str, ...], obs: tuple[str, ...], unc: tuple[str, ...] = (), atm: tuple[str, ...] = ()) -> None
¶
open_aviris(path: str | Path, product: str | None = None, wl_range: tuple[float, float] | None = None, good_bands_only: bool = False, geometry: bool = True, extras: bool = True, uncertainty: bool = False, fix_geometry: bool | str = 'auto', sort_bands: bool = True, ortho: bool = True, fill: float | None = None, map_coords: bool = False, chunks: str | int | dict | None = 'auto') -> xr.Dataset
¶
Open an AVIRIS-Classic, -NG, -3 or -5 flightline.
| PARAMETER | DESCRIPTION |
|---|---|
path
|
the cube, its
TYPE:
|
product
|
TYPE:
|
wl_range
|
TYPE:
|
good_bands_only
|
NaN out the water-vapour bands, keeping the band count
so indices stay aligned with the instrument's own grid. Uses the
product's
TYPE:
|
geometry
|
attach the OBS layers -
TYPE:
|
extras
|
attach the atmospheric state -
TYPE:
|
uncertainty
|
attach the per-band posterior uncertainty cube where one ships (AVIRIS-3, -5). Doubles the data volume.
TYPE:
|
fix_geometry
|
TYPE:
|
ortho
|
AVIRIS-5 only, and only for L1B. Its radiance ships on the raw
2000 x 1239 sensor grid with a lookup table, unlike its already
gridded L2A reflectance, so this applies that GLT to put the two
levels on the same map grid.
TYPE:
|
sort_bands
|
put the bands in ascending wavelength order. AVIRIS-Classic
reads out four spectrometers whose ranges overlap at the joins, so
its wavelengths step backwards three times - 667.5 -> 655.5 nm at
band 31, and again at 95 and 159. That leaves the coordinate
non-monotonic, and
TYPE:
|
fill
|
override the no-data value. Otherwise taken from the ENVI
header's
TYPE:
|
map_coords
|
add exact 2-D
TYPE:
|
chunks
|
dask chunking for the cube.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Dataset
|
|
Dataset
|
|
Dataset
|
|
| RAISES | DESCRIPTION |
|---|---|
FileNotFoundError
|
no cube matched, or a directory held none. |
ValueError
|
the filename is not a recognised AVIRIS granule, or a
directory holds both products and |
Source code in hyperproc/readers/aviris.py
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | |
_identify(path: Path) -> tuple[_Variant, str]
¶
Work out the instrument and granule id from a path.
Checks the path's own name first, then walks up - a bare directory such as
ang20220224t210144_rfl_v2aa1/ names the granule even though the files
inside repeat it.
Source code in hyperproc/readers/aviris.py
_find_cube(path: Path, variant: _Variant, granule: str, product: str | None) -> tuple[Path, str]
¶
Resolve a cube path, accepting the binary, its .hdr, or a directory.
Source code in hyperproc/readers/aviris.py
_guess_product(cube: Path, variant: _Variant) -> str
¶
Source code in hyperproc/readers/aviris.py
_search(start: Path, granule: str, globs: tuple[str, ...], roots: list[Path] | None = None) -> Path | None
¶
Find a granule's sibling file, looking in nearby directories.
NG and Classic split a flightline into *_rdn_* and *_rfl_* folders
and put the OBS in the radiance one, so opening reflectance means looking
one level up and back down.
Source code in hyperproc/readers/aviris.py
_sibling(cube: Path, granule: str, globs: tuple[str, ...]) -> Path | None
¶
read_hdr(path: str | Path) -> dict[str, str]
¶
Parse an ENVI .hdr into a dict of raw strings.
Handles the quirks in the AVIRIS family: brace-delimited values spanning
many lines, leading whitespace on keys (Classic indents wavelength and
fwhm), and map info ={ with no space before the brace.
Source code in hyperproc/readers/aviris.py
_good_bands(wl: np.ndarray, bbl: np.ndarray | None) -> tuple[np.ndarray, str]
¶
The usable-band flag, and where it came from.
AVIRIS-3 is the only variant that ships a bbl. For the other three the
flag is derived from :data:WATER_BANDS so the coordinate - and the band
CSV written from it - is populated for every instrument rather than blank
on three of four. good_bands_source in the attrs records which it is,
because the provider's judgement and ours are not the same thing.
Source code in hyperproc/readers/aviris.py
_fill_value(hdr: dict, variant: _Variant, product: str) -> float
¶
What counts as no-data, preferring what the header actually says.
Source code in hyperproc/readers/aviris.py
_hdr_array(hdr: dict, key: str) -> np.ndarray | None
¶
_chunks_for(cube: Path, spec, nx: int | None = None, nbands: int | None = None)
¶
Turn chunks="auto" into a spec that matches how ENVI stores the cube.
Both interleaves AVIRIS uses are line-major - BIL keeps a whole line of
every band together, BIP a whole line of every sample - so GDAL's block is
one image line. Dask's own "auto" chunks per band instead, which makes a
small spatial subset pull every band in full: slicing 200x200 pixels out of
a 11 GB flightline reads all 11 GB. Chunking along y only, keeping all
bands and samples together, turns that into one sequential read.
Source code in hyperproc/readers/aviris.py
_open_envi(cube: Path, variant: _Variant, granule: str, product: str, wl_range, chunks, fill, sort_bands=True) -> xr.Dataset
¶
Open one of the three ENVI variants.
Source code in hyperproc/readers/aviris.py
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 | |
_hdr_for(cube: Path) -> Path
¶
ENVI headers are either cube.hdr or cube.<ext>.hdr.
Source code in hyperproc/readers/aviris.py
_rotation(hdr: dict) -> float
¶
_read_spc(cube: Path, granule: str) -> tuple[np.ndarray | None, np.ndarray | None]
¶
Classic keeps band centres and widths in a two-column .spc.
Source code in hyperproc/readers/aviris.py
_gain(cube: Path, granule: str) -> np.ndarray | None
¶
Classic radiance is DN / gain; the sidecar holds one factor per band.
NG and AVIRIS-3 ship float radiance already in uW nm-1 cm-2 sr-1 and have no gain file, so this returns None for them.
Source code in hyperproc/readers/aviris.py
_band_subset(wl: np.ndarray | None, wl_range) -> np.ndarray | None
¶
Source code in hyperproc/readers/aviris.py
_open_av5(cube: Path, granule: str, wl_range, uncertainty, chunks, fill=None, ortho=True) -> xr.Dataset
¶
AVIRIS-5 ships CF-1.6 NetCDF, but its two levels are not on the same grid.
L2A reflectance (*_RFL_ORT.nc) is already orthorectified - 2009 x 3605
here, with easting/northing at the file root. L1B radiance
(*_L1B_RDN_*_RDN.nc) is not: it is the raw 2000 x 1239 sensor grid
with a lookup table alongside it, exactly like the OBS file. So the reader
applies that GLT to put radiance on the same map grid as reflectance, which
is what makes the two levels comparable pixel for pixel.
Source code in hyperproc/readers/aviris.py
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 | |
_gather(block, li, si, void)
¶
_glt_cube(cube: xr.DataArray, sample: np.ndarray, line: np.ndarray, tile: int = 512) -> xr.DataArray
¶
Orthorectify a (wavelength, y, x) cube through a JPL lookup table.
Done lazily and in tiles. The lazy part matters because the output here is 424 x 2009 x 3605 floats - 12 GB against the sensor grid's 4 GB, most of it the void around a rotated flight strip - so materialising it to hand back a dataset would defeat opening the cube lazily at all.
The tiling matters just as much. Gathering into chunks that span the whole grid means any spatial subset, however small, computes every pixel of every band in the chunk: a 100 x 100 window took minutes. Because the lookup table is spatially coherent, each output tile only ever reads a bounded window of the sensor grid, which is computed here at graph-build time.
Indices are 1-based with 0 for "no data"; a negative index marks a cell filled from a neighbour rather than sampled directly, so both signs point at a real pixel and only the zeros are dropped.
Source code in hyperproc/readers/aviris.py
_av5_cube_group(path: Path) -> str
¶
Name of the group holding the 3-D cube.
AVIRIS-5 does not use one name for it: reflectance sits under
reflectance/reflectance but its uncertainty sits under
uncertainty/uncertainty, so the group is looked up rather than guessed.
Source code in hyperproc/readers/aviris.py
_av5_grid(cube: Path) -> tuple[str, tuple]
¶
CRS and GDAL-order transform from the transverse_mercator variable.
Source code in hyperproc/readers/aviris.py
_as_str(v) -> str
¶
_apply_glt(raw: np.ndarray, sample: np.ndarray, line: np.ndarray) -> np.ndarray
¶
Put a raw (line, sample) layer onto the ortho grid.
JPL's lookup tables are 1-based with 0 for "no data", and a negative index marks a cell filled from a neighbour rather than sampled directly. Both signs point at a real pixel, so only the zeros are dropped.
Source code in hyperproc/readers/aviris.py
_finish(ds: xr.Dataset, var: str, units: str, product: str) -> None
¶
Coordinate metadata and the 1-D x/y every reader in the package sets.
Source code in hyperproc/readers/aviris.py
_add_map_coords(ds: xr.Dataset) -> None
¶
Exact per-pixel easting/northing, which a rotated grid needs.
Source code in hyperproc/readers/aviris.py
_blank_water_bands(ds: xr.Dataset) -> None
¶
NaN the water-vapour bands, keeping the band count.
Uses the product's own bbl when it has one; AVIRIS-3 flags 35 of 284
bands. Classic, NG and AVIRIS-5 ship no flag, so :data:WATER_BANDS stands
in - the windows AVIRIS-3's list marks.
Source code in hyperproc/readers/aviris.py
_add_geometry(ds: xr.Dataset, cube: Path, variant: _Variant, granule: str, chunks) -> None
¶
Attach the OBS layers and derive the relative azimuth.
Source code in hyperproc/readers/aviris.py
_add_geometry_envi(ds: xr.Dataset, obs: Path, chunks) -> None
¶
Source code in hyperproc/readers/aviris.py
_add_geometry_av5(ds: xr.Dataset, obs: Path) -> None
¶
AVIRIS-5 keeps OBS on the sensor grid; its own GLT puts it on the map.
Source code in hyperproc/readers/aviris.py
_elevation_array(src: str, band: int, glt: str | None, stamp: tuple, ny: int, nx: int) -> np.ndarray | None
cached
¶
Read (and if needed orthorectify) a DEM, remembering the last few.
Reading it eagerly is what makes :func:check_geometry possible, but a
notebook opens the same granule repeatedly and each open was re-reading
0.2-0.5 GB and redoing the GLT gather. Four entries is 40-115 MB each
depending on grid, so at most a few hundred MB held against opens that
would otherwise cost seconds apiece.
stamp is the source file's (mtime, size); it is in the key so an
edited or replaced sidecar is not served from a stale entry.
Source code in hyperproc/readers/aviris.py
_add_elevation(ds: xr.Dataset, cube: Path, variant: _Variant, granule: str) -> None
¶
Attach a per-pixel DEM, orthorectifying it first if need be.
Topographic correction needs elevation, and so does any honest check of the slope layer - which is the point: with a DEM in hand the slope convention can be tested rather than assumed per instrument.
Only AVIRIS-3 delivers it already gridded (LOC_ORT). NG and Classic
hand over the raw sensor grid (loc/ort_igm) plus a lookup table, so
those get the same GLT treatment as AVIRIS-5's OBS.
Source code in hyperproc/readers/aviris.py
_add_extras(ds: xr.Dataset, cube: Path, variant: _Variant, granule: str, chunks) -> None
¶
Atmospheric state: AOT and water vapour, however the variant ships it.