Geo¶
Sensor data and GeoTensor operators, behind the [geo] extra
(pip install "radtrax[geo]"). Today: Sentinel-2 SRFs from the agency table
georeader bundles, integrated by radtrax's
SpectralResponseFunction; a Sentinel-2 L1C image read by
georeader as a BandInstrument, a per-pixel ViewingGeometry and TOA
radiance; Landsat 8/9 OLI B6 and B7 and TIRS B10 and B11 from the USGS
spectral responses radtrax bundles, with radiance from Level-1 digital
numbers and the MTL rescaling and a per-pixel ViewingGeometry from the
Collection 2 angle bands (no extra needed); an EMIT L1B image read by georeader's EMITImage as a
BandInstrument with Gaussian bands of its FWHM and a per-pixel
ViewingGeometry from its obs file; and the matched filter as
pipekit operators on GeoTensors, around the one core in
radtrax.retrieval.matched_filter, with targets from the
single-slab observation model or, through UnitAbsorptionTarget, from v0's
ForwardModel, and MBMP, the
Sentinel-2 multi-band multi-pass signal (geotoolz's former plume.SBMP).
Landsat's thermal bands report radiance per µm, while the forward model
works per cm⁻¹: landsat_instrument(satellite, nu_cm, bands=("B10", "B11"),
per_um=True) takes radiance per cm⁻¹ and returns the band average per µm,
with the conversion inside the band integral. landsat_noise turns the
median on-orbit NEΔT at 300 K (LANDSAT_NETD_K: 0.048 K and 0.054 K for
TIRS, 0.048 K and 0.066 K for TIRS-2) into radiance noise through that
instrument; LANDSAT_CALIBRATION_K holds TIRS-2's vicarious calibration
residual (0.16 K and 0.61 K). Both tables come from J. Barsi's
presentation at the ESA Living Planet Symposium 2022 (session H2-02), as
quoted in radtrax#225. The TIRS responses are the USGS band-average RSR workbooks
(L8_TIRS_RSR.xlsx, L9_TIRS2_RSR.xlsx), recorded with their SHA-256 in
the bundled table's header.
ECOSTRESS's five thermal bands, B1 to B5 at about 8.3, 8.8, 9.2, 10.5 and 12.1 µm, follow the same pattern from plain inputs, no extra needed: ecostress_instrument(nu_cm, bands=...) integrates the science team's spectral responses (revision 3, as the EUMETSAT NWP SAF redistributes them, bundled with their SHA-256 in the table's header) and returns band radiance per µm by default, the L1B unit. Bands 1 and 3 hold fill values from 15 May 2019 until five-band mode resumed on 28 April 2023 (ECOSTRESS_3BAND_PERIOD), and the two modes have been interleaved since, so ecostress_bands reads a granule's band set from its BandSpecification metadata (the date alone decides only before 28 April 2023); the three-band set is ECOSTRESS_3BAND, bands 2, 4 and 5. ecostress_noise uses ECOSTRESS_NETD_K, 0.1 K per band at 300 K: that is the requirement (NEΔT < 0.1 K, ECOSTRESS L2 LSTE ATBD, 2018), an upper bound until per-band on-orbit values are sourced. ecostress_radiance stacks the L1B radiance_<n> datasets with their fill values as NaN, and ecostress_geometry builds a per-pixel ViewingGeometry from the L1B geolocation angles.
geo
¶
Sensor data and GeoTensor operators, behind the [geo] extra.
- :func:
sentinel2_srfreads the Sentinel-2 SRF table georeader bundles into radtrax's SRF (radtrax#28); :func:sentinel2_band_responsesgives its raw B11 and B12 responses on a wavelength grid (radtrax#115). - :func:
sentinel2_instrument, :func:sentinel2_geometryand :func:sentinel2_radiance(:mod:radtrax.geo.sentinel2): a Sentinel-2 L1C image read by georeader as aBandInstrument, a per-pixelViewingGeometryand TOA radiance (radtrax#120). - :func:
landsat_srf, :func:landsat_instrument, :func:landsat_radianceand :func:landsat_geometry(:mod:radtrax.geo.landsat): Landsat 8/9 OLI B6 and B7 from the USGS spectral responses radtrax bundles, radiance from Level-1 digital numbers and the MTL rescaling, and a per-pixelViewingGeometryfrom the Collection 2 angle bands (radtrax#121); the TIRS bands B10 and B11 with band radiance per µm, and their NEΔT as a noise model, :func:landsat_noise(radtrax#225); no extra needed. - :func:
ecostress_bands, :func:ecostress_srf, :func:ecostress_instrument, :func:ecostress_noise, :func:ecostress_radianceand :func:ecostress_geometry(:mod:radtrax.geo.ecostress): ECOSTRESS's five thermal bands and its three-band mode, from the science team's spectral responses radtrax bundles, band radiance per µm, NEΔT as a noise model, L1B radiance and a per-pixelViewingGeometryfrom the geolocation angles (radtrax#226); no extra needed. - :func:
emit_instrumentand :func:emit_geometry(:mod:radtrax.geo.emit): an EMIT L1B image read by georeader'sEMITImageas aBandInstrumentwith Gaussian bands of its FWHM, and a per-pixelViewingGeometryfrom its obs file (radtrax#114). - :mod:
radtrax.geo.matched_filter: pipekit operators on GeoTensors around the one matched-filter core (radtrax#29), ported from geotoolz; :class:UnitAbsorptionTargetgives them v0'sForwardModeltarget (radtrax#98). - :class:
MBMP(:mod:radtrax.geo.mbmp): the Sentinel-2 multi-band multi-pass signal, geotoolz's formerplume.SBMP(radtrax#30), with optional per-pass scale factors; :class:SBMPand :class:MBSPcomplete radtrax · 2's multi-pass family (radtrax#111).
Importing this subpackage does not need the extra; the operators load on
first use, and a missing extra raises an ImportError naming it.
srf¶
Sentinel-2 spectral response functions from georeader's bundled table.
georeader keeps the agency SRF tables as sensor data (COPE-GSEG-EOPG-TN-15-0007
v4.0, bundled, so no network); radtrax owns the band integration
(:class:radtrax.instrument.SpectralResponseFunction). This module reads the
one into the other (radtrax#28). It needs the [geo] extra.
sentinel2_srf
¶
sentinel2_srf(satellite: str = 'S2A', *, wavelengths_hr_nm: Sequence[float] | Float[ArrayLike, ' Ghr'] | None = None, bands: Sequence[str] | None = None) -> SpectralResponseFunction
Build a Sentinel-2 SRF from the agency table georeader bundles.
georeader drops the table's rows where every band is at most 1e-6, so the table is first restored onto its full 1 nm grid with zeros there; interpolating across a dropped gap would invent a response.
The 1 nm responses are then projected onto wavelengths_hr_nm, not
sampled at it. With φ_j the linear-interpolation (hat) basis on the
grid, band b's weight on grid point j is
W[b, j] = Σ_k R_b(λ_k) φ_j(λ_k) over the table's wavelengths. Band
integration Σ_j W[b, j] L(λ_j) / Σ_j W[b, j] is then exact for any
radiance that is linear between grid points, and every band keeps its full
response on a coarse grid. Sampling instead loses most of a narrow band at
5–10 nm, and a band can vanish entirely from about 25 nm. On the table's
own 1 nm grid the two coincide.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
satellite
|
str
|
|
'S2A'
|
wavelengths_hr_nm
|
Sequence[float] | Float[ArrayLike, ' Ghr'] | None
|
The high-resolution grid [nm] to integrate on, strictly increasing. Defaults to the table's 1 nm grid. |
None
|
bands
|
Sequence[str] | None
|
Band names to keep, in order (e.g. |
None
|
Returns:
| Type | Description |
|---|---|
SpectralResponseFunction
|
A |
SpectralResponseFunction
|
wavelengths and its widths the equivalent widths |
SpectralResponseFunction
|
both computed on the 1 nm table. Its |
SpectralResponseFunction
|
projected weights divided by the grid's trapezoid bin widths, which |
SpectralResponseFunction
|
|
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the grid misses more than |
Raises:
| Type | Description |
|---|---|
ImportError
|
If the |
KeyError
|
If a requested band is not in the table. |
Source code in src/radtrax/geo/srf.py
sentinel2_band_responses
¶
sentinel2_band_responses(satellite: str = 'S2A', wavelengths_nm: Float[ArrayLike, ' G'] | None = None) -> tuple[Float[ndarray, ' G'], Float[ndarray, ' G']]
Sentinel-2 band 11 and 12 responses, interpolated onto a wavelength grid.
The raw agency responses (not normalised), zero outside the table, as
:func:radtrax.spectroscopy.synthetic.methane_cross_section takes them
(radtrax · 1 and · 2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
satellite
|
str
|
|
'S2A'
|
wavelengths_nm
|
Float[ArrayLike, ' G'] | None
|
The grid [nm], |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Float[ndarray, ' G'], Float[ndarray, ' G']]
|
|
Source code in src/radtrax/geo/srf.py
sentinel2¶
Sentinel-2 L1C on radtrax's terms: instrument, geometry and radiance.
georeader's S2ImageL1C reads the product: TOA reflectance as digital
numbers, the sun and per-band viewing angles on the 5 km tie-point grid of
the tile metadata, and the band solar irradiance. This module turns them
into the three things a forward model takes (radtrax#120):
- :func:
sentinel2_instrument: a :class:~radtrax.instrument.BandInstrumentfor the SWIR pair B11 and B12 on the model's wavenumber grid, from the agency SRFs (:func:~radtrax.geo.sentinel2_srf); - :func:
sentinel2_geometry: a :class:~radtrax.atmosphere.ViewingGeometryper pixel, its angles interpolated from the tie points onto the image grid; - :func:
sentinel2_radiance: TOA radiance from TOA reflectance,
with E_b the band solar irradiance, d the Earth–Sun distance [AU]
(the product's U = 1/d²) and θ_s the solar zenith angle of the pixel.
The tie-point grid. The L1C metadata gives each angle at the nodes of a
5 km grid whose first node is the tile's upper-left corner. georeader's
sza/saa/vza/vaa GeoTensors place that first value at the
centre of a 5 km cell instead, 2.5 km off in both directions; its own
anglesULXY is the origin that puts the nodes on the corner. The angles
are re-georeferenced on anglesULXY here, so bilinear interpolation onto
the image grid uses the nodes where the metadata puts them, and a tile's
pixels all lie inside the node grid (23 nodes span 110 km, a tile 109.8 km).
The functions need the [geo] extra; this module imports without it.
sentinel2_instrument
¶
sentinel2_instrument(satellite: str, nu_cm: Float[ArrayLike, ' G'], *, bands: Sequence[str] = ('B11', 'B12'), shift_cm: float = 0.0) -> BandInstrument
Sentinel-2 bands as a :class:~radtrax.instrument.BandInstrument.
The agency SRFs (:func:~radtrax.geo.sentinel2_srf) projected onto the
wavelengths of the model grid, λ_g = 10⁷ / ν_g, and turned onto the
wavenumber grid by :meth:BandInstrument.from_srf
<radtrax.instrument.BandInstrument.from_srf>:
with f the SRF's row-normalised matrix, columns reversed. At zero
shift it is :meth:SpectralResponseFunction.apply
<radtrax.instrument.SpectralResponseFunction.apply> of the same SRF on
the spectrum in ascending wavelength.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
satellite
|
str
|
|
required |
nu_cm
|
Float[ArrayLike, ' G']
|
The model's wavenumber grid [cm⁻¹], strictly increasing,
|
required |
bands
|
Sequence[str]
|
Band names, in order. |
('B11', 'B12')
|
shift_cm
|
float
|
Spectral shift |
0.0
|
Returns:
| Type | Description |
|---|---|
BandInstrument
|
The instrument, |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the grid misses more than |
Raises:
| Type | Description |
|---|---|
ImportError
|
If the |
KeyError
|
If a band is not in the SRF table. |
ValueError
|
If |
Examples:
>>> import numpy as np
>>> nu = np.arange(4150.0, 6700.0) # 1493–2410 nm, B11 and B12
>>> instrument = sentinel2_instrument("S2A", nu)
>>> instrument.band_names
('B11', 'B12')
>>> [round(float(y), 12) for y in instrument(np.ones(nu.size))]
[1.0, 1.0]
Source code in src/radtrax/geo/sentinel2.py
sentinel2_geometry
¶
sentinel2_geometry(image: Any, band: str = 'B12') -> ViewingGeometry
Per-pixel viewing geometry of an L1C image, on its grid (H, W).
The solar zenith θ_s, the viewing zenith θ_v of band and the
relative azimuth
sensor minus sun (φ = 0 is the backscatter hotspot, as
:mod:radtrax.surface.ross_li reads it), are taken from the tie-point
grid (see the module docstring) and interpolated bilinearly onto the
image's pixel centres. φ is formed on the tie points, before the
interpolation, so azimuths that wrap through 0°/360° do not average
across the cut; φ itself is continuous.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Any
|
A georeader |
required |
band
|
str
|
The band whose viewing angles to use; Sentinel-2's detectors see each band from a slightly different direction. |
'B12'
|
Returns:
| Type | Description |
|---|---|
ViewingGeometry
|
A |
ViewingGeometry
|
every pixel, built under |
ViewingGeometry
|
|
ViewingGeometry
|
hands one pixel's geometry to a forward model. |
Raises:
| Type | Description |
|---|---|
ImportError
|
If the |
KeyError
|
If the metadata has no viewing angles for |
ValueError
|
If the sun and view azimuth grids differ. |
RuntimeError
|
If an interpolated cosine is outside |
Source code in src/radtrax/geo/sentinel2.py
sentinel2_radiance
¶
TOA radiance from an L1C image's reflectance, per band and pixel.
with DN_b the digital numbers georeader reads (the radiometric
offset of processing baseline 04.00 already added), Q the
quantification value (10 000), E_b the band solar irradiance
[W m⁻² nm⁻¹] and U = 1/d² from the product metadata, and θ_s
each pixel's solar zenith angle, interpolated as in
:func:sentinel2_geometry. ESA computes the L1C reflectance with the
per-pixel angle, so this inverts it exactly; georeader's
DN_to_radiance uses the tile's mean angle instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Any
|
A georeader |
required |
bands
|
Sequence[str]
|
Band names, in order. |
('B11', 'B12')
|
Returns:
| Type | Description |
|---|---|
Any
|
A |
Any
|
on the image grid. Fill pixels ( |
Any
|
|
Raises:
| Type | Description |
|---|---|
ImportError
|
If the |
Source code in src/radtrax/geo/sentinel2.py
emit¶
EMIT L1B on radtrax's terms: instrument and per-pixel geometry.
georeader's EMITImage reads an EMIT L1B radiance granule: the per-band
centre wavelengths and fwhm [nm] of its current band selection, and,
through the companion observation (obs) file, the sun and sensor angles of
every pixel, georeferenced through the granule's geographic lookup table
(GLT) by EMITImage.observation(name). This module turns them into what a
forward model takes (radtrax#114):
- :func:
emit_instrument: a :class:~radtrax.instrument.BandInstrumenton the model's wavenumber grid, with Gaussian bands of the image's FWHM (exact FWHM, :data:~radtrax.instrument.FWHM_PER_SIGMA, radtrax#28); - :func:
emit_geometry: a :class:~radtrax.atmosphere.ViewingGeometryper pixel of the image grid, from the obs file's four angle bands.
Neither function imports georeader: they read the image's attributes and
call its methods, so they need no extra beyond whatever produced the image
(EMITImage itself comes with the [geo] extra).
emit_instrument
¶
emit_instrument(image: Any, nu_cm: Float[ArrayLike, ' G'], *, bands: Sequence[int] | None = None, shift_cm: float = 0.0) -> BandInstrument
EMIT bands as a :class:~radtrax.instrument.BandInstrument on a wavenumber grid.
Band b is a Gaussian in wavelength with the image's centre \(c_b\)
and FWHM \(w_b\),
sampled at \(\lambda_g = 10^7 / \nu_g\) by
:class:~radtrax.instrument.SpectralResponseFunction (trapezoidal
weights, rows normalised to 1) and turned onto the wavenumber grid by
:meth:BandInstrument.from_srf <radtrax.instrument.BandInstrument.from_srf>.
A band of which the grid holds less than 99.9 % of the Gaussian's area, \(\Phi((\lambda_{\max} - c_b)/\sigma_b) - \Phi((\lambda_{\min} - c_b)/\sigma_b)\), is dropped, with one warning naming every dropped band: a band is kept when its centre lies at least \(3.09\,\sigma_b \approx 1.31\,w_b\) inside the grid's wavelength range (a one-sided tail of 0.1 %).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Any
|
A georeader |
required |
nu_cm
|
Float[ArrayLike, ' G']
|
The model's wavenumber grid [cm⁻¹], strictly increasing,
|
required |
bands
|
Sequence[int] | None
|
Indices into |
None
|
shift_cm
|
float
|
Spectral shift |
0.0
|
Returns:
| Type | Description |
|---|---|
BandInstrument
|
The instrument, |
BandInstrument
|
is named |
BandInstrument
|
so the names say which bands were kept. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If bands are dropped because the grid does not cover them. |
Raises:
| Type | Description |
|---|---|
GridError
|
If |
ShapeError
|
If |
ParameterError
|
If a band index is out of range, or a FWHM is not positive. |
Examples:
>>> import numpy as np
>>> from types import SimpleNamespace
>>> image = SimpleNamespace(
... wavelengths=np.array([1000.0, 2300.0, 2310.0]),
... fwhm=np.array([8.0, 8.5, 8.5]),
... )
>>> nu = np.arange(4250.0, 4400.0, 0.1) # 2273–2353 nm
>>> instrument = emit_instrument(image, nu, bands=[1, 2])
>>> instrument.band_names
('B1', 'B2')
>>> [round(float(y), 12) for y in instrument(np.ones(nu.size))]
[1.0, 1.0]
Source code in src/radtrax/geo/emit.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
emit_geometry
¶
emit_geometry(image: Any) -> ViewingGeometry
Per-pixel viewing geometry of an EMIT image, on its grid (H, W).
The solar zenith \(\theta_s\), the sensor zenith \(\theta_v\) and the relative azimuth
sensor minus sun, both azimuths clockwise from north toward the sun and
toward the sensor (φ = 0 is the backscatter hotspot, as
:func:~radtrax.geo.sentinel2_geometry forms it), from the obs file's
bands as image.observation(name) georeferences them (nearest
sensor pixel through the GLT, no interpolation).
Pixels outside the swath (the GLT's no-data) and obs fill values get the
mean of the valid pixels' angles, so that the whole grid is a valid
ViewingGeometry; mask them with the radiance's fill value or
image.valid_glt, as for any other per-pixel product.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Any
|
A georeader |
required |
Returns:
| Type | Description |
|---|---|
ViewingGeometry
|
A |
ViewingGeometry
|
|
Raises:
| Type | Description |
|---|---|
MissingKeyError
|
If the obs file lacks one of the four angle bands. |
ShapeError
|
If the four angle grids differ in shape. |
DegenerateInputError
|
If no pixel has valid angles. |
RuntimeError
|
If a valid zenith angle is 90° or more
( |
Source code in src/radtrax/geo/emit.py
landsat¶
Landsat 8/9 OLI and TIRS: SRF, instrument, noise, radiance and geometry.
OLI (Landsat 8) and OLI-2 (Landsat 9) carry a SWIR band pair, B6 at 1.6 µm and B7 at 2.2 µm, like Sentinel-2's B11 and B12, so the multi-pass family and the band-table inversion apply unchanged (radtrax#121). TIRS (Landsat 8) and TIRS-2 (Landsat 9) add the thermal pair, B10 at about 10.9 µm and B11 at about 12 µm (radtrax#225). georeader has neither a Collection 2 Level-1 reader (spaceml-org/georeader#99) nor the Landsat spectral responses (spaceml-org/georeader#100), so this module works on plain inputs:
- :func:
landsat_srfand :func:landsat_instrument: the USGS band-average relative spectral responses, bundled with radtrax (geo/data/landsat_oli_rsr.csv, B6, B7, B10 and B11 of each satellite; its header records the source workbooks, their SHA-256 and the retrieval date), as a :class:~radtrax.instrument.SpectralResponseFunctionand a :class:~radtrax.instrument.BandInstrument; - :func:
landsat_noise: the thermal bands' NEΔT (LANDSAT_NETD_K) as an :class:~radtrax.instrument.NEdTNoise; the TIRS-2 calibration residual isLANDSAT_CALIBRATION_K; - :func:
landsat_radiance: TOA radiance from the Level-1 digital numbers and the MTL's rescaling,L = M Q + A; - :func:
landsat_geometry: a per-pixel :class:~radtrax.atmosphere.ViewingGeometryfrom the four Collection 2 angle bands (SZA, SAA, VZA, VAA).
Units. Level-1 radiance is per µm (W m⁻² sr⁻¹ µm⁻¹, the MTL's unit); the
forward model's is per cm⁻¹. landsat_instrument(..., per_um=True) takes
radiance per cm⁻¹ and returns band averages per µm, the conversion inside
the band integral (:meth:BandInstrument.from_srf
<radtrax.instrument.BandInstrument.from_srf>), so its output, the noise
:func:landsat_noise builds on it and :func:landsat_radiance share one
unit.
The table is long-format (satellite, band, wavelength, response). None of
these functions needs the [geo] extra; :func:landsat_radiance returns a
GeoTensor when it is given one.
landsat_srf
¶
landsat_srf(satellite: str = 'L8', *, bands: Sequence[str] = ('B6', 'B7'), wavelengths_hr_nm: Sequence[float] | Float[ArrayLike, ' Ghr'] | None = None) -> SpectralResponseFunction
Build a Landsat 8/9 SRF from the bundled USGS band-average RSRs.
Each band's response is placed on one 1 nm grid spanning the requested
bands, zero where it was not measured, and projected onto
wavelengths_hr_nm with the linear-interpolation (hat) basis, as
:func:~radtrax.geo.sentinel2_srf does: band integration is then exact
for any radiance linear between grid points. OLI's responses are
published every 1 nm and used as they are; TIRS's every 50 nm, and are
taken as piecewise linear between the published values (interpolated
onto the 1 nm grid). The values are used as published, including the
few small negative values in Landsat 8 OLI's tails and the floor of
about 1e-3 Landsat 8 TIRS reports across 9–14 µm (so a grid narrower
than 9–14 µm misses more than 0.1 % of an L8 thermal band, and warns).
The responses are relative to radiance (energy), as the Landsat 8 TIRS
workbook states, so the band average is ∫R L dλ / ∫R dλ.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
satellite
|
str
|
|
'L8'
|
bands
|
Sequence[str]
|
Band names, in order; |
('B6', 'B7')
|
wavelengths_hr_nm
|
Sequence[float] | Float[ArrayLike, ' Ghr'] | None
|
The high-resolution grid [nm] to integrate on, strictly increasing. Defaults to the table's 1 nm grid. |
None
|
Returns:
| Type | Description |
|---|---|
SpectralResponseFunction
|
A |
SpectralResponseFunction
|
wavelengths and widths the equivalent widths |
SpectralResponseFunction
|
computed on the 1 nm table. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the grid misses more than |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
KeyError
|
If a band is not bundled. |
Examples:
>>> srf = landsat_srf("L9")
>>> srf.band_names
('B6', 'B7')
>>> [round(float(c)) for c in srf.band_centers_nm]
[1608, 2201]
>>> tirs = landsat_srf("L9", bands=("B10", "B11"))
>>> [round(float(c)) for c in tirs.band_centers_nm]
[10837, 12025]
Source code in src/radtrax/geo/landsat.py
landsat_instrument
¶
landsat_instrument(satellite: str, nu_cm: Float[ArrayLike, ' G'], *, bands: Sequence[str] = ('B6', 'B7'), shift_cm: float = 0.0, per_um: bool = False) -> BandInstrument
Landsat 8/9 bands as a :class:~radtrax.instrument.BandInstrument.
The RSRs (:func:landsat_srf) projected onto the wavelengths of the
model grid, λ_g = 10⁷ / ν_g, and turned onto the wavenumber grid by
:meth:BandInstrument.from_srf
<radtrax.instrument.BandInstrument.from_srf>:
with f the SRF's row-normalised matrix, columns reversed. At zero
shift it is :meth:SpectralResponseFunction.apply
<radtrax.instrument.SpectralResponseFunction.apply> of the same SRF on
the spectrum in ascending wavelength.
For the thermal bands pass per_um=True: the instrument then takes
radiance per cm⁻¹ [W m⁻² sr⁻¹ (cm⁻¹)⁻¹] and returns the band average per
µm [W m⁻² sr⁻¹ µm⁻¹], the unit of :func:landsat_radiance, with
f_bg = w_bg ν_g² / 10⁴ (rows no longer summing to 1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
satellite
|
str
|
|
required |
nu_cm
|
Float[ArrayLike, ' G']
|
The model's wavenumber grid [cm⁻¹], strictly increasing,
|
required |
bands
|
Sequence[str]
|
Band names, in order. |
('B6', 'B7')
|
shift_cm
|
float
|
Spectral shift |
0.0
|
per_um
|
bool
|
Band radiance per µm from radiance per cm⁻¹. |
False
|
Returns:
| Type | Description |
|---|---|
BandInstrument
|
The instrument, |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the grid misses more than |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a band is not bundled. |
ValueError
|
If |
Examples:
>>> import numpy as np
>>> nu = np.arange(4200.0, 6650.0) # 1504–2381 nm, B6 and B7
>>> instrument = landsat_instrument("L8", nu)
>>> instrument.band_names
('B6', 'B7')
>>> [round(float(y), 6) for y in instrument(np.ones(nu.size))]
[1.0, 1.0]
>>> nu_tir = np.arange(700.0, 1120.0, 0.5) # 8.9–14.3 µm, B10 and B11
>>> tirs = landsat_instrument(
... "L9", nu_tir, bands=("B10", "B11"), per_um=True
... )
>>> [round(float(y), 9) for y in tirs(2.0 * 1e4 / nu_tir**2)] # 2 per µm
[2.0, 2.0]
Source code in src/radtrax/geo/landsat.py
landsat_noise
¶
landsat_noise(satellite: str, instrument: BandInstrument, *, t_ref_k: Real[ArrayLike, ''] = 300.0) -> NEdTNoise
The thermal bands' NEΔT as a noise model on instrument's bands.
with \(\bar B_b\) the Planck radiance integrated by instrument and
NEΔT the median on-orbit value at 300 K (LANDSAT_NETD_K). \(σ\) is in
the instrument's units: build it with per_um=True to compare \(σ\)
with :func:landsat_radiance. The same \(σ\) is a different NEΔT at
another scene temperature (:meth:NEdTNoise.netd_at
<radtrax.instrument.NEdTNoise.netd_at>), larger in a colder scene. The
calibration residual (LANDSAT_CALIBRATION_K) is systematic and is
not part of this noise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
satellite
|
str
|
|
required |
instrument
|
BandInstrument
|
The thermal bands, from :func: |
required |
t_ref_k
|
Real[ArrayLike, '']
|
The temperature NEΔT is quoted at [K], a scalar. |
300.0
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
NEdTNoise
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
KeyError
|
If a band has no tabulated NEΔT (OLI's B6 and B7). |
Examples:
>>> import numpy as np
>>> nu = np.arange(700.0, 1120.0, 0.5)
>>> tirs = landsat_instrument("L9", nu, bands=("B10", "B11"), per_um=True)
>>> sigma = landsat_noise("L9", tirs).radiance_sigma(np.zeros(2))
>>> [f"{float(s):.2e}" for s in sigma] # W m⁻² sr⁻¹ µm⁻¹
['6.92e-03', '8.00e-03']
Source code in src/radtrax/geo/landsat.py
landsat_radiance
¶
landsat_radiance(dn: Real[ArrayLike, ' B *shape'], *, bands: Sequence[str] = ('B6', 'B7'), mtl: Mapping[str, Any] | None = None, mult: Sequence[float] | Float[ArrayLike, ' B'] | None = None, add: Sequence[float] | Float[ArrayLike, ' B'] | None = None) -> Float[ndarray, ' B *shape']
TOA radiance from Level-1 digital numbers, per band and pixel.
with Q_b the calibrated digital numbers and M_b, A_b the
MTL's RADIANCE_MULT_BAND_n and RADIANCE_ADD_BAND_n (group
LEVEL1_RADIOMETRIC_RESCALING). Q = 0 is fill (no data) in
Collection 2 and stays 0. The thermal bands B10 and B11 rescale the same
way; their radiance is a band average per µm, so model it with
landsat_instrument(..., per_um=True). The product resamples TIRS's
100 m samples onto the 30 m grid; no point-spread or ground-sampling
operator is applied here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dn
|
Real[ArrayLike, ' B *shape']
|
The digital numbers, |
required |
bands
|
Sequence[str]
|
Band names, in order, |
('B6', 'B7')
|
mtl
|
Mapping[str, Any] | None
|
A parsed MTL (the |
None
|
mult
|
Sequence[float] | Float[ArrayLike, ' B'] | None
|
|
None
|
add
|
Sequence[float] | Float[ArrayLike, ' B'] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Float[ndarray, ' B *shape']
|
Radiance [W m⁻² sr⁻¹ µm⁻¹, the unit of the MTL rescaling], float64, |
Float[ndarray, ' B *shape']
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither or both of |
KeyError
|
If the MTL has no rescaling for a band. |
Examples:
>>> import numpy as np
>>> dn = np.array([[[0, 10_000]], [[20_000, 30_000]]], dtype=np.uint16)
>>> landsat_radiance(dn, mult=[1e-3, 2e-4], add=[-5.0, -1.0]).tolist()
[[[0.0, 5.0]], [[3.0, 5.0]]]
Source code in src/radtrax/geo/landsat.py
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
landsat_geometry
¶
landsat_geometry(sza: Real[ArrayLike, ' H W'], saa: Real[ArrayLike, ' H W'], vza: Real[ArrayLike, ' H W'], vaa: Real[ArrayLike, ' H W'], *, scale_deg: float = _ANGLE_SCALE_DEG) -> ViewingGeometry
Per-pixel viewing geometry from the Collection 2 angle bands.
The four Level-1 angle bands (_SZA, _SAA, _VZA, _VAA,
computed for band 4) are signed 16-bit integers in hundredths of a
degree (LSDS-1822 v7.0, section 3.4), hence scale_deg = 0.01; pass
scale_deg=1.0 for angles already in degrees. The angle bands are
computed for band 4 (OLI) only, and serve the thermal bands B10 and B11
here as well; a difference between OLI's and TIRS's lines of sight, if
any, is not modelled. The relative azimuth
sensor minus sun, as :func:~radtrax.geo.sentinel2_geometry forms it,
does not depend on whether the azimuths are stored in [−180°, 180°]
or [0°, 360°).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sza
|
Real[ArrayLike, ' H W']
|
Solar zenith angle, |
required |
saa
|
Real[ArrayLike, ' H W']
|
Solar azimuth angle, |
required |
vza
|
Real[ArrayLike, ' H W']
|
Viewing (sensor) zenith angle, |
required |
vaa
|
Real[ArrayLike, ' H W']
|
Viewing (sensor) azimuth angle, |
required |
scale_deg
|
float
|
Degrees per stored unit. |
_ANGLE_SCALE_DEG
|
Returns:
| Type | Description |
|---|---|
ViewingGeometry
|
A |
ViewingGeometry
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the four bands differ in shape. |
RuntimeError
|
If a cosine is outside |
Examples:
>>> import numpy as np
>>> sza, saa = np.full((1, 2), 3000), np.array([[15000, -17000]])
>>> vza, vaa = np.zeros((1, 2)), np.array([[-6000, 10000]])
>>> g = landsat_geometry(sza, saa, vza, vaa)
>>> np.rad2deg(np.asarray(g.relative_azimuth_rad)).round(3).tolist()
[[150.0, 90.0]]
Source code in src/radtrax/geo/landsat.py
ecostress¶
ECOSTRESS: band set, SRF, instrument, noise, radiance and geometry.
ECOSTRESS (PHyTIR, on the ISS) measures five thermal bands, B1 to B5 at
about 8.3, 8.8, 9.2, 10.5 and 12.1 µm (radtrax#226). From 15 May 2019 to
28 April 2023 bands 1 and 3 hold fill values only (the three-band mode,
ECOSTRESS_3BAND, after the mass-storage anomaly); since 28 April 2023
five-band acquisitions are interleaved with three-band ones, so the band set
is a property of the granule (its BandSpecification metadata), not of the
date alone. georeader has no ECOSTRESS reader, so this module works on plain
inputs, the arrays and metadata an HDF5 reader returns:
- :func:
ecostress_bands: the granule's bands fromBandSpecification, or from the acquisition date inside the three-band period; - :func:
ecostress_srfand :func:ecostress_instrument: the science team's spectral responses (revision 3), bundled with radtrax (geo/data/ecostress_srf.csv; its header records the source, the SHA-256 of each file and the retrieval date), as a :class:~radtrax.instrument.SpectralResponseFunctionand a :class:~radtrax.instrument.BandInstrument; - :func:
ecostress_noise: NEΔT (ECOSTRESS_NETD_K, the < 0.1 K requirement as an upper bound) as an :class:~radtrax.instrument.NEdTNoise; - :func:
ecostress_radiance: the L1Bradiance_<n>datasets stacked per band, fill values as NaN; - :func:
ecostress_geometry: a per-pixel :class:~radtrax.atmosphere.ViewingGeometryfrom the L1B geolocation product's solar and view angles.
Units. L1B radiance is per µm (W m⁻² sr⁻¹ µm⁻¹, ECOSTRESS L1 User Guide,
table 3.1.1); the forward model's is per cm⁻¹. ecostress_instrument
therefore defaults to per_um=True: it takes radiance per cm⁻¹ and returns
band averages per µm, the conversion inside the band integral
(:meth:BandInstrument.from_srf <radtrax.instrument.BandInstrument.from_srf>),
so its output, the noise built on it and :func:ecostress_radiance share one
unit. None of these functions needs the [geo] extra.
ecostress_bands
¶
ecostress_bands(band_specification: Sequence[float] | Float[ArrayLike, ' C'] | None = None, *, acquired: date | str | None = None) -> tuple[str, ...]
The TIR bands with data in a granule.
The L1 products' BandSpecification metadata (group
L1B_RADMetadata or L1GEOMetadata) lists six wavelengths [µm],
the SWIR band then B1 to B5, with 0 for a band that holds fill values
(ECOSTRESS L1 User Guide, D-57151). A band is valid when its entry is
not 0. Without the metadata, the acquisition date decides inside the
three-band period (ECOSTRESS_3BAND_PERIOD) and before it (five
bands); after it the two modes are interleaved, so the date is not
enough.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
band_specification
|
Sequence[float] | Float[ArrayLike, ' C'] | None
|
The |
None
|
acquired
|
date | str | None
|
The acquisition date, a |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The valid bands, in order, from |
...
|
|
Raises:
| Type | Description |
|---|---|
ParameterError
|
If neither argument is given, or only a date on or after 28 April 2023. |
ShapeError
|
If |
DegenerateInputError
|
If the metadata marks every TIR band as fill. |
Examples:
>>> ecostress_bands([0.0, 0.0, 8.7, 0.0, 10.5, 12.0])
('B2', 'B4', 'B5')
>>> ecostress_bands(acquired="2018-08-01")
('B1', 'B2', 'B3', 'B4', 'B5')
>>> ecostress_bands(acquired="2021-07-01")
('B2', 'B4', 'B5')
Source code in src/radtrax/geo/ecostress.py
ecostress_srf
¶
ecostress_srf(*, bands: Sequence[str] | None = None, wavelengths_hr_nm: Sequence[float] | Float[ArrayLike, ' Ghr'] | None = None) -> SpectralResponseFunction
Build the ECOSTRESS SRF from the bundled science-team responses.
The responses (revision 3, geo/data/ecostress_srf.csv) are
published on a wavenumber grid about 9.85 nm apart in wavelength; each
band is taken as piecewise linear in wavelength between the samples,
placed on one 1 nm grid and projected onto wavelengths_hr_nm with
the linear-interpolation (hat) basis, as
:func:~radtrax.geo.landsat_srf does. The band average is
∫S L dλ / ∫S dλ, as in the L2 LSTE ATBD (2018, eq. 3).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bands
|
Sequence[str] | None
|
Band names, in order, from |
None
|
wavelengths_hr_nm
|
Sequence[float] | Float[ArrayLike, ' Ghr'] | None
|
The high-resolution grid [nm] to integrate on, strictly increasing. Defaults to the 1 nm grid. |
None
|
Returns:
| Type | Description |
|---|---|
SpectralResponseFunction
|
A |
SpectralResponseFunction
|
wavelengths and widths the equivalent widths |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the grid misses more than |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a band is not one of |
ValueError
|
If the grid is not 1-D and strictly increasing. |
Examples:
>>> srf = ecostress_srf()
>>> [round(float(c) / 1e3, 2) for c in srf.band_centers_nm] # µm
[8.29, 8.79, 9.2, 10.49, 12.1]
Source code in src/radtrax/geo/ecostress.py
ecostress_instrument
¶
ecostress_instrument(nu_cm: Float[ArrayLike, ' G'], *, bands: Sequence[str] | None = None, shift_cm: float = 0.0, per_um: bool = True) -> BandInstrument
ECOSTRESS bands as a :class:~radtrax.instrument.BandInstrument.
The SRFs (:func:ecostress_srf) projected onto the wavelengths of the
model grid, λ_g = 10⁷ / ν_g, and turned onto the wavenumber grid by
:meth:BandInstrument.from_srf
<radtrax.instrument.BandInstrument.from_srf>. With per_um=True (the
default) the instrument takes radiance per cm⁻¹
[W m⁻² sr⁻¹ (cm⁻¹)⁻¹] and returns the band average per µm
[W m⁻² sr⁻¹ µm⁻¹], the unit of :func:ecostress_radiance:
Build it with the granule's bands (:func:ecostress_bands): the band
tuple is static, so filter_jit compiles one model per mode, and the
three-band instrument's rows are the five-band one's rows of B2, B4, B5.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nu_cm
|
Float[ArrayLike, ' G']
|
The model's wavenumber grid [cm⁻¹], strictly increasing,
|
required |
bands
|
Sequence[str] | None
|
Band names, in order; all five by default. |
None
|
shift_cm
|
float
|
Spectral shift |
0.0
|
per_um
|
bool
|
Band radiance per µm from radiance per cm⁻¹; |
True
|
Returns:
| Type | Description |
|---|---|
BandInstrument
|
The instrument, |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the grid misses more than |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a band is not one of |
ValueError
|
If |
Examples:
>>> import numpy as np
>>> nu = np.arange(760.0, 1300.0, 0.5) # 7.7–13.2 µm
>>> instrument = ecostress_instrument(nu, bands=ECOSTRESS_3BAND)
>>> instrument.band_names
('B2', 'B4', 'B5')
>>> [round(float(y), 9) for y in instrument(3.0 * 1e4 / nu**2)] # 3 per µm
[3.0, 3.0, 3.0]
Source code in src/radtrax/geo/ecostress.py
ecostress_noise
¶
ecostress_noise(instrument: BandInstrument, *, netd_k: Mapping[str, float] | None = None, t_ref_k: Real[ArrayLike, ''] = 300.0) -> NEdTNoise
NEΔT as a noise model on instrument's bands.
with \(\bar B_b\) the Planck radiance integrated by instrument. The
default NEΔT, ECOSTRESS_NETD_K, is the 0.1 K requirement at 300 K
for every band: an upper bound, not an on-orbit measurement. A uniform
NEΔT is not a uniform radiance noise: \(\partial \bar B / \partial T\)
at 300 K is about 1.5 times larger at 8.3 µm than at 12.1 µm. \(σ\) is in
the instrument's units (per µm with the default per_um=True), and is
a larger NEΔT in a colder scene (:meth:NEdTNoise.netd_at
<radtrax.instrument.NEdTNoise.netd_at>).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
BandInstrument
|
The bands, from :func: |
required |
netd_k
|
Mapping[str, float] | None
|
NEΔT per band [K] at |
None
|
t_ref_k
|
Real[ArrayLike, '']
|
The temperature NEΔT is quoted at [K], a scalar. |
300.0
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
NEdTNoise
|
class: |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a band has no NEΔT in |
Examples:
>>> import numpy as np
>>> nu = np.arange(760.0, 1300.0, 0.5)
>>> eco = ecostress_instrument(nu, bands=("B2", "B5"))
>>> sigma = ecostress_noise(eco).radiance_sigma(np.zeros(2))
>>> [f"{float(s):.2e}" for s in sigma] # W m⁻² sr⁻¹ µm⁻¹
['1.78e-02', '1.20e-02']
Source code in src/radtrax/geo/ecostress.py
ecostress_radiance
¶
ecostress_radiance(radiance: Mapping[str, Real[ArrayLike, ' H W']], *, bands: Sequence[str] | None = None) -> Float[ndarray, ' B H W']
L1B radiance per band and pixel, fill values as NaN.
Stacks the ECO_L1B_RAD datasets radiance_1 to radiance_5 (group
Radiance), already calibrated radiance per µm
[W m⁻² sr⁻¹ µm⁻¹, ECOSTRESS L1 User Guide, table 3.1.1]; the special
values -9997 (pixel not seen), -9998 (missing stripe, not filled in) and
-9999 (missing or bad data) become NaN, as does every pixel of a band a
three-band granule fills. Model it with
ecostress_instrument(..., per_um=True).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
radiance
|
Mapping[str, Real[ArrayLike, ' H W']]
|
The datasets by name ( |
required |
bands
|
Sequence[str] | None
|
Band names, in order; all five by default. Pass
:func: |
None
|
Returns:
| Type | Description |
|---|---|
Float[ndarray, ' B H W']
|
Radiance [W m⁻² sr⁻¹ µm⁻¹], float64, |
Raises:
| Type | Description |
|---|---|
KeyError
|
If a band is unknown or its dataset is missing. |
ShapeError
|
If the datasets differ in shape. |
Examples:
>>> import numpy as np
>>> rad = {
... "radiance_2": np.array([[8.5, -9998.0]]),
... "radiance_4": np.array([[9.25, 9.5]]),
... }
>>> ecostress_radiance(rad, bands=("B2", "B4")).tolist()
[[[8.5, nan]], [[9.25, 9.5]]]
Source code in src/radtrax/geo/ecostress.py
ecostress_geometry
¶
ecostress_geometry(solar_zenith: Real[ArrayLike, ' H W'], solar_azimuth: Real[ArrayLike, ' H W'], view_zenith: Real[ArrayLike, ' H W'], view_azimuth: Real[ArrayLike, ' H W']) -> ViewingGeometry
Per-pixel viewing geometry from the L1B geolocation angles.
ECO_L1B_GEO holds solar_zenith, solar_azimuth, view_zenith
and view_azimuth [deg] for every radiance pixel (group
Geolocation, ECOSTRESS L1 User Guide, table 4.1.1); the ISS's
precessing orbit makes them vary from granule to granule, so they are
taken per pixel. The relative azimuth
sensor minus sun, as :func:~radtrax.geo.landsat_geometry forms it,
does not depend on whether the azimuths are stored in [−180°, 180°]
or [0°, 360°). The user guide does not say whether view_azimuth
points toward or away from the sensor; it is read as toward the sensor,
as EMIT's obs file and Landsat's VAA are.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
solar_zenith
|
Real[ArrayLike, ' H W']
|
Solar zenith angle [deg], |
required |
solar_azimuth
|
Real[ArrayLike, ' H W']
|
Solar azimuth angle [deg], |
required |
view_zenith
|
Real[ArrayLike, ' H W']
|
Sensor zenith angle [deg], |
required |
view_azimuth
|
Real[ArrayLike, ' H W']
|
Sensor azimuth angle [deg], |
required |
Returns:
| Type | Description |
|---|---|
ViewingGeometry
|
A |
ViewingGeometry
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the four grids differ in shape. |
RuntimeError
|
If a cosine is outside |
Examples:
>>> import numpy as np
>>> g = ecostress_geometry(
... np.full((1, 2), 60.0),
... np.array([[100.0, -170.0]]),
... np.full((1, 2), 20.0),
... np.array([[-80.0, 120.0]]),
... )
>>> np.rad2deg(np.asarray(g.relative_azimuth_rad)).round(3).tolist()
[[180.0, 70.0]]
Source code in src/radtrax/geo/ecostress.py
matched_filter¶
GeoTensor operators for the matched filter (behind the [geo] extra).
pipekit Operator wrappers around the one matched-filter core,
:mod:radtrax.retrieval.matched_filter — ported from geotoolz's
matched_filter operators (radtrax#29), which now delegate here. They keep
geotoolz's carrier contract: a GeoTensor in gives a GeoTensor out on
the same grid (a plain array gives a plain array), score maps are a new
quantity holding NaN at nodata pixels with fill_value_default=NaN,
and estimators return non-carrier results (vectors, covariance operators,
backgrounds).
Layout: the band axis is -3 (georeader's (C, H, W) /
(T, C, H, W)), so these operators move it to the core's band-last
(H, W, B) and back. On a (T, C, H, W) stack the background is fitted
once over the pixels of every frame and score maps come back
(T, 1, H, W).
Nodata: a pixel that is non-finite or equals the carrier's
fill_value_default in any band is left out of every statistic.
Precision: the core is JAX; the operators run it in float64
(jax.enable_x64 for the call only), matching geotoolz's NumPy results
whatever the global JAX setting. Only the operators do: a covariance operator
they return (EstimateCov*, MatchedFilter.cov_op_,
StreamingBackgroundResult.cov_op) holds float64 arrays, and using it
directly with the core under float32 JAX truncates it.
Conventions that differ from geotoolz's own implementation:
- covariances are the
1/n(maximum-likelihood) scatter, as scikit-learn and the rest of radtrax; geotoolz used1/(n − 1); - :class:
AdaptiveWindowBackgroundgives the population variance, takes aguard_windowand always reflects at the edges (nopad_mode); - :class:
StreamingBackgroundnames its shrinkage"ledoit_wolf"/"oas"(geotoolz:"shrunk"); - :class:
GMMClusterBackgroundfits scikit-learn's Gaussian mixture.
AdaptiveBackground
¶
Bases: NamedTuple
Local window statistics, in the layout of the cube they came from.
Attributes:
| Name | Type | Description |
|---|---|---|
mean |
Cube
|
Per-pixel local mean, the cube's shape. |
variance |
Cube
|
Per-pixel per-band local (population) variance, the cube's shape. |
StreamingBackgroundResult
¶
Bases: NamedTuple
Background from :class:StreamingBackground.
Attributes:
| Name | Type | Description |
|---|---|---|
mean |
Spectrum
|
Mean spectrum |
cov_op |
AbstractLinearOperator
|
Covariance operator |
MatchedFilter
¶
MatchedFilter(*, mean: SpectrumLike | None = None, cov_op: CovLike | None = None, target: SpectrumLike | None = None, fit_on_call: bool = False, mean_method: MeanMethod = 'median', cov_method: CovMethod = 'ledoit_wolf', axis: int = -3)
Bases: Operator
Per-pixel matched-filter score map.
y = (x − μ)ᵀ Σ⁻¹ t / (tᵀ Σ⁻¹ t) for every pixel, through
:func:radtrax.retrieval.matched_filter.apply_image, as a single-band
map on the input's carrier.
A fitted operator: :meth:fit estimates whichever of mean /
cov_op the constructor left unset (with mean_method /
cov_method) into mean_ / cov_op_; :meth:transform scores
with that background; calling the operator fits the missing statistics
once, on the first call, or — with fit_on_call=True — estimates them
from every cube without storing anything. Fitted arrays never reach
get_config().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
SpectrumLike | None
|
Background mean spectrum |
None
|
cov_op
|
CovLike | None
|
Background covariance, a lineax operator or a |
None
|
target
|
SpectrumLike | None
|
Target signature |
None
|
fit_on_call
|
bool
|
Estimate the background from each cube (per scene) instead of fitting the missing statistics once. |
False
|
mean_method
|
MeanMethod
|
Mean estimator when fitting. Default |
'median'
|
cov_method
|
CovMethod
|
|
'ledoit_wolf'
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Attributes:
| Name | Type | Description |
|---|---|---|
mean_ |
Spectrum | None
|
Fitted mean, or |
cov_op_ |
CovLike | None
|
Fitted covariance operator, or |
Source code in src/radtrax/geo/matched_filter.py
fit
¶
fit(gt: Carrier) -> MatchedFilter
Estimate the unset background statistics from the valid pixels of gt.
Returns:
| Type | Description |
|---|---|
MatchedFilter
|
This operator, for chaining. |
Source code in src/radtrax/geo/matched_filter.py
transform
¶
Score gt against the fitted (or supplied) background.
Raises:
| Type | Description |
|---|---|
ValueError
|
If no background is available or |
Source code in src/radtrax/geo/matched_filter.py
MatchedFilterPixel
¶
Bases: Operator
Matched-filter score of one pixel spectrum, as a float.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
SpectrumLike
|
Background mean spectrum |
required |
cov_op
|
CovLike
|
Background covariance, a lineax operator or a |
required |
target
|
SpectrumLike
|
Target signature |
required |
Source code in src/radtrax/geo/matched_filter.py
MatchedFilterSNR
¶
Bases: Operator
Theoretical detection SNR amplitude · √(tᵀ Σ⁻¹ t).
A graph source: computed from its configuration; an optional input is
accepted and ignored, so it can close a Sequential.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amplitude
|
float
|
Target amplitude. |
required |
cov_op
|
CovLike
|
Background covariance, a lineax operator or a |
required |
target
|
SpectrumLike
|
Target signature |
required |
Source code in src/radtrax/geo/matched_filter.py
DetectionThreshold
¶
Bases: Operator
Score threshold with a given false-alarm rate under the Gaussian null.
A graph source, like :class:MatchedFilterSNR.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
false_alarm_rate
|
float
|
Exceedance probability, in |
required |
cov_op
|
CovLike
|
Background covariance, a lineax operator or a |
required |
target
|
SpectrumLike
|
Target signature |
required |
Source code in src/radtrax/geo/matched_filter.py
ValidateMFInputs
¶
Bases: Operator
Pass-through guard: fail early on a zero target or a singular covariance.
Returns its input unchanged, whatever its type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_op
|
CovLike
|
Background covariance, a lineax operator or a |
required |
target
|
SpectrumLike
|
Target signature |
required |
Source code in src/radtrax/geo/matched_filter.py
EstimateMean
¶
EstimateMean(*, method: MeanMethod = 'mean', trim_proportion: float = 0.1, huber_c: float = 1.345, axis: int = -3)
Bases: Operator
Background mean spectrum (B,) of a cube's valid pixels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
MeanMethod
|
|
'mean'
|
trim_proportion
|
float
|
Fraction cut from each tail for |
0.1
|
huber_c
|
float
|
Huber threshold in MAD units for |
1.345
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
EstimateCovEmpirical
¶
Bases: Operator
Empirical (1/n) covariance operator of a cube's valid pixels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
SpectrumLike | None
|
Mean spectrum |
None
|
ridge
|
float
|
Diagonal ridge added to the covariance. |
0.0
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
EstimateCovShrunk
¶
EstimateCovShrunk(*, mean: SpectrumLike | None = None, method: ShrinkMethod = 'ledoit_wolf', axis: int = -3)
Bases: Operator
Ledoit–Wolf or OAS covariance operator of a cube's valid pixels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
SpectrumLike | None
|
Mean spectrum |
None
|
method
|
ShrinkMethod
|
|
'ledoit_wolf'
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
EstimateCovLowRank
¶
EstimateCovLowRank(*, mean: SpectrumLike | None = None, rank: int = LOWRANK_RANK, tikhonov: float = LOWRANK_TIKHONOV, seed: int | None = 0, n_oversamples: int = 10, axis: int = -3)
Bases: Operator
Low-rank plus Tikhonov covariance λI + V D Vᵀ of a cube's valid pixels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
SpectrumLike | None
|
Mean spectrum |
None
|
rank
|
int
|
Leading components kept. |
LOWRANK_RANK
|
tikhonov
|
float
|
Diagonal floor |
LOWRANK_TIKHONOV
|
seed
|
int | None
|
Seed of the randomised SVD. |
0
|
n_oversamples
|
int
|
Extra random directions beyond |
10
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
GMMClusterBackground
¶
GMMClusterBackground(*, n_clusters: int, cov_estimator: Literal['empirical', 'ledoit_wolf', 'oas'] = 'ledoit_wolf', seed: int | None = 0, bayesian: bool = False, axis: int = -3)
Bases: Operator
Per-cluster backgrounds from a Gaussian mixture over the valid pixels.
Returns a :class:~radtrax.retrieval.matched_filter.ClusterBackground
whose labels have the cube's spatial shape (-1 at nodata pixels);
wire it into :class:ApplyClusterMF.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
Number of mixture components. |
required |
cov_estimator
|
Literal['empirical', 'ledoit_wolf', 'oas']
|
Per-cluster covariance, |
'ledoit_wolf'
|
seed
|
int | None
|
Seed of the mixture fit. |
0
|
bayesian
|
bool
|
Use a Bayesian mixture, which prunes unused components. |
False
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
ApplyClusterMF
¶
Bases: Operator
Matched filter with each pixel's own cluster background.
The background arrives at call time, so the config (target,
axis) stays YAML-safe: ApplyClusterMF(target=t)(gt, background)
with the background from :class:GMMClusterBackground.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
SpectrumLike
|
Target signature |
required |
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
AdaptiveWindowBackground
¶
Bases: Operator
Per-pixel local mean and variance over a square window.
Returns an :class:AdaptiveBackground of two cubes in the input's layout
(not a carrier); nodata pixels carry no weight and are NaN in both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
window
|
int
|
Side length of the window, an odd integer |
7
|
guard_window
|
int
|
Side length of a central guard region left out of each
window ( |
0
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
ApplyAdaptiveMF
¶
Bases: Operator
Matched filter against each pixel's local diagonal background.
α = tᵀ Λ⁻¹ (x − μ) / (tᵀ Λ⁻¹ t) with Λ = diag(σ² + ridge) from
:class:AdaptiveWindowBackground, passed at call time:
ApplyAdaptiveMF(target=t)(gt, background).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
SpectrumLike
|
Target signature |
required |
ridge
|
float
|
Non-negative variance floor. Default |
0.0
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
StreamingBackground
¶
StreamingBackground(*, cov_kind: Literal['empirical', 'ledoit_wolf', 'oas'] = 'ledoit_wolf', ridge: float = 1e-08, axis: int = -3)
Bases: Operator
Mean and covariance accumulated over an iterable of cubes.
Streams the valid pixels of every cube (GeoTensor or array) through
:func:radtrax.retrieval.matched_filter.streaming_background; the
cubes arrive at call time, so the config stays YAML-safe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov_kind
|
Literal['empirical', 'ledoit_wolf', 'oas']
|
|
'ledoit_wolf'
|
ridge
|
float
|
Diagonal ridge added to the covariance. Default |
1e-08
|
axis
|
int
|
Position of the band axis in each cube. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
LinearTargetFromObs
¶
LinearTargetFromObs(*, obs_model: Any, vmr_background: VMRField | Cube | None = None, pattern: str | VMRField | Cube = 'uniform', pixel: Pixel | None = None, axis: int = -3, fd_step: float | None = None)
Bases: Operator
Tangent-linear target signature (B,) from an observation model.
With a
:class:~radtrax.retrieval.var3d.RadianceObservationModel the target is
the exact JVP of its forward model
(:func:radtrax.retrieval.matched_filter.linear_target_from_obs) about
vmr_background (default: the model's reference VMR on the cube's
grid; pass it on the high-resolution grid when the model has a GSD).
Any other callable maps a state cube to a response with its band axis at
axis (or to a (B,) vector), about vmr_background (default:
the cube itself, its nodata pixels set to the band means of the valid
ones); a cube response is averaged over the valid pixels. A JAX-traceable
callable is linearised exactly with :func:jax.jvp; one that is not
(NumPy code converting its input) by a central difference, which is only
as good as its step: fd_step when given, else 1e-4 times the
state's largest magnitude (1e-4 itself for an all-zero state, where
the state carries no scale — set fd_step from the model's then). (geotoolz always took a
one-sided step of 1e-4 in the state's own units, far outside the
linear regime for a VMR of ~2e-6.)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obs_model
|
Any
|
An observation model, or a callable. |
required |
vmr_background
|
VMRField | Cube | None
|
Background state; see above. |
None
|
pattern
|
str | VMRField | Cube
|
|
'uniform'
|
pixel
|
Pixel | None
|
Impulse location on the state's grid; an observation model
reads the target at the output pixel holding it ( |
None
|
axis
|
int
|
Position of the band axis. Default |
-3
|
fd_step
|
float | None
|
Absolute central-difference step for a callable that JAX cannot trace; see above. |
None
|
Source code in src/radtrax/geo/matched_filter.py
NonlinearTargetFromObs
¶
NonlinearTargetFromObs(*, obs_model: Any, vmr_background: VMRField | Cube | None = None, amplitude: float = 1.0, pattern: str | VMRField | Cube = 'uniform', pixel: Pixel | None = None, axis: int = -3)
Bases: Operator
Finite-amplitude target H(x_b + α δx) − H(x_b), (B,).
Keeps the forward model's nonlinearity at amplitude α. With a
:class:~radtrax.retrieval.var3d.RadianceObservationModel it is
:func:radtrax.retrieval.matched_filter.nonlinear_target_from_obs about
vmr_background (default: the model's reference VMR on the cube's
grid). Any other callable maps a state cube to a response with its band
axis at axis (or to a (B,) vector), about vmr_background
(default: the cube, nodata pixels set to the valid band means); a cube
response is averaged over the valid pixels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obs_model
|
Any
|
An observation model, or a callable. |
required |
vmr_background
|
VMRField | Cube | None
|
Background state; see above (the incoming cube for a callable). |
None
|
amplitude
|
float
|
Perturbation amplitude |
1.0
|
pattern
|
str | VMRField | Cube
|
|
'uniform'
|
pixel
|
Pixel | None
|
Impulse location for |
None
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
UnitAbsorptionTarget
¶
UnitAbsorptionTarget(*, model: ForwardModel, atmosphere: AtmosphericState, surface: Surface, geometry: ViewingGeometry, gas: str = 'CH4', profile: Float[ndarray, ' L'] | None = None, log_cube: bool = False, mean_method: MeanMethod = 'median', axis: int = -3)
Bases: Operator
Target (B,) from v0's layered :class:~radtrax.ForwardModel.
The scene's unit absorption spectrum
t = ∂ ln y / ∂ΔX at ΔX = 0 [per mol/m²], from
:func:~radtrax.unit_absorption_target on the background atmosphere,
surface and geometry (one geometry for the scene, e.g. its mean
angles), turned into the target of the cube it is called on:
- a cube of radiance
y(default):μ ⊙ t, withμthe cube's background mean (:class:EstimateMean,mean_method), since∂y/∂ΔX = y t; the matched filter then scoresΔXin mol/m²; - a cube of
ln y(log_cube=True):titself.
The single-slab :class:~radtrax.retrieval.var3d.RadianceObservationModel
goes to :class:LinearTargetFromObs instead (radtrax#98).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
ForwardModel
|
The forward model; its instrument's bands are the cube's bands, in order. |
required |
atmosphere
|
AtmosphericState
|
The background atmospheric state ( |
required |
surface
|
Surface
|
The surface (a Lambertian albedo cancels from |
required |
geometry
|
ViewingGeometry
|
Sun and sensor directions, one for the scene. |
required |
gas
|
str
|
The plume's gas. Default |
'CH4'
|
profile
|
Float[ndarray, ' L'] | None
|
Relative weight of the plume per layer, |
None
|
log_cube
|
bool
|
Whether the cube holds |
False
|
mean_method
|
MeanMethod
|
Mean estimator for |
'median'
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Raises:
| Type | Description |
|---|---|
ShapeError
|
(on call) If the model's band count differs from the cube's. |
Source code in src/radtrax/geo/matched_filter.py
ColumnEnhancement
¶
ColumnEnhancement(*, gas: str = 'CH4', sensor: str = 'EMIT', obs_model: Any | None = None, mean_method: MeanMethod = 'median', cov_method: ShrinkMethod | Literal['lowrank'] = 'ledoit_wolf', target_pattern: str = 'uniform', vmr_background: VMRField | Cube | None = None, target_pixel: Pixel | None = None, fd_step: float | None = None, rank: int | None = None, tikhonov: float | None = None, axis: int = -3)
Bases: Operator
Trace-gas enhancement map: mean, covariance, target and matched filter.
Chains :class:EstimateMean, :class:EstimateCovShrunk (or
:class:EstimateCovLowRank), :class:LinearTargetFromObs (a uniform
unit target without obs_model) and :class:MatchedFilter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gas
|
str
|
Gas label (metadata only). Default |
'CH4'
|
sensor
|
str
|
Sensor label (metadata only). Default |
'EMIT'
|
obs_model
|
Any | None
|
Observation model or callable for the target. |
None
|
mean_method
|
MeanMethod
|
Mean estimator. Default |
'median'
|
cov_method
|
ShrinkMethod | Literal['lowrank']
|
|
'ledoit_wolf'
|
target_pattern
|
str
|
Perturbation pattern of the target. |
'uniform'
|
vmr_background
|
VMRField | Cube | None
|
Background state of the target, forwarded to
:class: |
None
|
target_pixel
|
Pixel | None
|
The target's |
None
|
fd_step
|
float | None
|
Central-difference step of the target for an |
None
|
rank
|
int | None
|
Rank for |
None
|
tikhonov
|
float | None
|
Floor for |
None
|
axis
|
int
|
Position of the band axis. Default |
-3
|
Source code in src/radtrax/geo/matched_filter.py
mbmp¶
The multi-pass operators on GeoTensors (behind the [geo] extra).
:class:MBMP is ported from geotoolz's plume.SBMP (radtrax#30),
renamed after radtrax · 2: with a reference scene it computes the
multi-band multi-pass log signal,
:func:radtrax.retrieval.mbmp.mbmp_log_signal, or with
scale_factors=True :func:radtrax.retrieval.mbmp.mbmp_signal; without
one, the single-pass prefilter :func:radtrax.retrieval.mbmp.swir_contrast.
:class:SBMP (one band, two passes) and :class:MBSP (two bands, one
pass) complete radtrax · 2's family (radtrax#111). Their scale factors
are fitted on every valid pixel of each pass (each time step of a stack); a
plume in the fit biases them, so fit on a background mask with the
functions in :mod:radtrax.retrieval.mbmp when a plume is large.
MBMP
¶
MBMP(*, swir1: BandRef = 'B11', swir2: BandRef = 'B12', axis: int = -3, eps: float = 1e-10, scale_factors: bool = False)
Bases: Operator
Sentinel-2 multi-band multi-pass methane signal, with or without scale factors.
Called as MBMP()(scene, reference_scene): band 11 against band 12 on
the observed and on a clean-air reference pass of the same pixel grid,
.. math:: s = \ln\frac{R_{11} + ε}{R_{12} + ε} - \ln\frac{R'{11} + ε}{R' ,} + ε
positive where a plume depresses band 12 (Varon et al., 2021). A
scene-wide change between the passes stays in :math:s as an offset
unless scale_factors subtracts ln(c / c′), each pass's factor
fitted on its valid pixels (geotoolz called the signal SBMP;
radtrax · 2 shows it is an MBMP). Without a reference scene the output
is the single-pass prefilter (R₁₁ − R₁₂) / (R₁₁ + R₁₂ + ε), not a
column retrieval. Negative values are clipped to zero first.
With a reference, a pixel whose band 11 or 12 is not positive on either
pass (shadow, water, noise, or a -9999 sentinel in an unnamed
reference) is NaN: its log would saturate at about
±ln(R/ε) ≈ ±22 and outrank every real plume (0.01–0.2).
geotoolz's SBMP scored those pixels.
Bands resolve through the carrier's names (attrs band_names,
descriptions, bands); only a plain array with exactly 12 bands
falls back to the Sentinel-2 L2A order (B1–B8, B8A, B9,
B11, B12), so "B11" never picks B10 on a 13-band L1C stack.
A GeoTensor reference resolves its own names; a plain one shares the
scene's band positions.
The output is single-band ((T, 1, H, W) for a stack), NaN where
either scene has a nodata pixel, with fill_value_default=NaN. The
band arithmetic runs in float64.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
swir1
|
BandRef
|
Band 11 (weak absorption), an index or a name. Default
|
'B11'
|
swir2
|
BandRef
|
Band 12 (strong absorption), an index or a name. Default
|
'B12'
|
axis
|
int
|
Band axis. Default |
-3
|
eps
|
float
|
Guard against log and division by zero. Default |
1e-10
|
scale_factors
|
bool
|
Fit and remove each pass's scale factor (radtrax#111).
Default |
False
|
Source code in src/radtrax/geo/mbmp.py
SBMP
¶
Bases: Operator
Sentinel-2 single-band multi-pass signal, −ln(c_s R₁₂ / R′₁₂).
Called as SBMP()(scene, reference_scene): band 12 on the observed
and on a reference pass of the same grid, with R′₁₂ ≈ c_s R₁₂
fitted on the valid pixels of each time step
(:func:radtrax.retrieval.mbmp.sbmp_signal). Invert through τ₁₂.
A scene-wide change between passes cancels; a local one (a wetted field)
reads as methane (radtrax · 2). Not geotoolz's SBMP, which was the
MBMP log signal (:class:MBMP).
Band resolution, NaN for nodata or non-positive pixels, the
single-band output and float64 arithmetic are as in :class:MBMP.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
swir2
|
BandRef
|
Band 12 (strong absorption), an index or a name. Default
|
'B12'
|
axis
|
int
|
Band axis. Default |
-3
|
eps
|
float
|
Guard against log of zero. Default |
1e-10
|
Source code in src/radtrax/geo/mbmp.py
MBSP
¶
Bases: Operator
Sentinel-2 multi-band single-pass signal, −ln(c R₁₂ / R₁₁).
Called as MBSP()(scene): band 11 against band 12 on one pass, with
R₁₁ ≈ c R₁₂ fitted on the valid pixels of each time step
(:func:radtrax.retrieval.mbmp.mbsp_signal). Invert through
τ₁₂ − τ₁₁. A surface whose band ratio departs from the scene's reads
as methane (radtrax · 2).
Band resolution, NaN for nodata or non-positive pixels, the
single-band output and float64 arithmetic are as in :class:MBMP.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
swir1
|
BandRef
|
Band 11 (weak absorption), an index or a name. Default
|
'B11'
|
swir2
|
BandRef
|
Band 12 (strong absorption), an index or a name. Default
|
'B12'
|
axis
|
int
|
Band axis. Default |
-3
|
eps
|
float
|
Guard against log of zero. Default |
1e-10
|