Instrument¶
From radiance to what a sensor records: the Instrument protocol, BandInstrument, JAX band integration with a differentiable spectral shift, and GaussianInstrument, Gaussian bands shifted analytically in cm⁻¹ or nm (spectral); spectral response functions and NumPy band integration (srf), point-spread and ground-sampling operators (psf); the NoiseModel protocol and its diagonal, SNR and NEΔT noise (noise).
instrument
¶
From radiance to what a sensor records.
srf integrates hyperspectral radiance over spectral response functions
(SpectralResponseFunction); psf holds the JAX point-spread and
ground-sampling operators with their hand-written adjoints. spectral
holds the Instrument protocol and BandInstrument, the JAX band
integration with a differentiable spectral shift (radtrax#36), and
GaussianInstrument, Gaussian bands shifted analytically in cm⁻¹ or nm
(radtrax#96). noise
holds the NoiseModel protocol, the measurement-noise covariance
(radtrax#108), with NEdTNoise for thermal bands (radtrax#249).
Ported from plumax 0.1.2 at 28f257e (plumax.radtran.srf,
plumax.radtran.instrument).
spectral¶
The Instrument protocol and a band instrument with a spectral shift.
An instrument turns radiance on the model's wavenumber grid into what the sensor records per band (radtrax · 3). v0's instrument integrates over each band's spectral response and lets the wavenumber scale shift:
.. math:: y_b = \sum_g f_{bg}\, L(ν_g + δ),
with f the row-normalised response matrix (B, G), L the
radiance (G,) and δ the spectral shift [cm⁻¹]: a positive δ
moves every band's response to higher wavenumber (shorter wavelength). A
uniform shift in wavenumber is not one in wavelength (δλ = −λ² δν / 10⁷):
across 1600–2500 nm the same δν moves the bands by amounts 2.4 times apart,
so a calibration given per nm converts band by band.
L(ν + δ) is linear interpolation on the grid, so y is piecewise
linear in δ with kinks at multiples of the grid step; on a node the
derivative is the slope of the segment above it. On a 0.01 cm⁻¹ grid that
derivative is within 0.2 % of an analytically shifted SRF's and a
Gauss–Newton step from δ = 0 converges; ∂²y/∂δ² is zero, so a
method that needs the curvature in δ needs the analytic shift. Beyond
the grid's ends the radiance is held at its edge value, so keep each band's
response well inside the grid. A changing FWHM and the PSF and GSD of
:mod:radtrax.instrument.psf join it in v1 of the roadmap.
The response matrix comes from the NumPy
:class:~radtrax.instrument.SpectralResponseFunction (exact Gaussian FWHM,
trapezoidal weights) through :meth:BandInstrument.from_srf, so both
integrate a band the same way; this module only moves it onto the gradient
path. Ported from the radtrax · 3 design notebook's Instrument.
:class:GaussianInstrument (radtrax#96) builds the same Gaussian bands in
JAX from their centres and FWHM in nm, and shifts the response rather than
the spectrum:
.. math:: y_b = \frac{\sum_g w_g\, f_b(ν_g)\, L(ν_g)}{\sum_g w_g\, f_b(ν_g)},\qquad f_b(ν) = \exp\Big(-\frac{(10^7/(ν - δ) - δ_λ - c_b)^2}{2σ_b^2}\Big),
with w the trapezoidal weights on the wavelengths 10⁷/ν, c_b the
centre, σ_b the FWHM over 2√(2 ln 2), and a shift δ in cm⁻¹ or
δ_λ in nm. At zero shift it is BandInstrument.from_srf of the same
bands; y is smooth in both shifts and in the FWHM, so ∂²y/∂δ² is
the band's curvature, not zero, and a calibration per nm needs no
conversion.
Shapes: G wavenumbers, B bands.
Instrument
¶
Bases: Module
Protocol: radiance on a wavenumber grid in, one value per band out.
An abstract Equinox module, so filter_jit compiles each concrete
instrument and a forward model can hold any of them.
The contract: an instrument is linear in radiance, y = A L with
A its response matrix (B, G) (:meth:response_matrix), so
I(a L₁ + b L₂) = a I(L₁) + b I(L₂). Its parameters (a shift, a FWHM)
may enter A non-linearly. Band brightness temperature and NEΔT
(:mod:radtrax.radiometry.brightness) rely on it.
Attributes:
| Name | Type | Description |
|---|---|---|
nu_cm |
AbstractVar[Float[Array, ' G']]
|
The wavenumber grid the instrument reads [cm⁻¹], |
response_matrix
¶
The response matrix A = ∂y/∂L, (B, G).
By default jax.jacrev of the call at zero radiance (B
reverse passes, exact for a linear instrument); an instrument that
holds A in closed form overrides it.
Source code in src/radtrax/instrument/spectral.py
BandInstrument
¶
BandInstrument(nu_cm: Float[ArrayLike, ' G'], response: Float[ArrayLike, 'B G'], shift_cm: Float[ArrayLike, ''] = 0.0, band_names: tuple[str, ...] | None = None)
Bases: Instrument
Band integration with a spectral shift, y_b = Σ_g f_bg L(ν_g + δ).
Attributes:
| Name | Type | Description |
|---|---|---|
nu_cm |
Float[Array, ' G']
|
Wavenumber grid [cm⁻¹], strictly increasing, |
response |
Float[Array, 'B G']
|
Spectral response per band on |
shift_cm |
Float[Array, '']
|
Spectral shift |
band_names |
tuple[str, ...]
|
One label per band. |
Examples:
>>> import jax.numpy as jnp
>>> instrument = BandInstrument(
... jnp.array([4300.0, 4300.1, 4300.2]),
... jnp.array([[0.5, 0.5, 0.0], [0.0, 0.5, 0.5]]),
... )
>>> [float(y) for y in instrument(jnp.array([1.0, 2.0, 4.0]))]
[1.5, 3.0]
Source code in src/radtrax/instrument/spectral.py
from_srf
classmethod
¶
from_srf(srf: SpectralResponseFunction, nu_cm: Float[ArrayLike, ' G'], *, shift_cm: Float[ArrayLike, ''] = 0.0, per_um: bool = False) -> BandInstrument
From a :class:~radtrax.instrument.SpectralResponseFunction built
on the wavelengths of nu_cm.
srf.wavelengths_hr_nm must be 1e7 / nu_cm in ascending
order (nu_cm reversed), so the response is the SRF's own matrix,
its columns reversed onto the wavenumber grid.
With per_um=True the instrument takes radiance per cm⁻¹ and
returns the band average per µm, which is what a thermal Level-1
product reports (Landsat TIRS, ECOSTRESS). The SRF's rows w are
weights in wavelength (R Δλ, summing to 1), so the band average
of \(L_\lambda = L_{\tilde\nu} \tilde\nu^2 / 10^4\) is
and the factor is folded into the response,
\(f_{bg} = w_{bg} \tilde\nu_g^2 / 10^4\), whose rows then sum to the
band's mean of \(\tilde\nu^2 / 10^4\) (about 84 at 10.9 µm), not to 1.
Averaging \(L_{\tilde\nu}\) with w and converting afterwards is not
the same: across a 0.9 µm band at 10.9 µm, \(\tilde\nu^2\) changes by
about ±8 %. The factor is taken at the unshifted grid, so a shift
\(\delta\) adds a relative error of about \(2\delta/\tilde\nu\)
(2e-4 for 0.1 cm⁻¹ at 900 cm⁻¹).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
srf
|
SpectralResponseFunction
|
The SRF, on |
required |
nu_cm
|
Float[ArrayLike, ' G']
|
Wavenumber grid [cm⁻¹], strictly increasing, |
required |
shift_cm
|
Float[ArrayLike, '']
|
Spectral shift |
0.0
|
per_um
|
bool
|
Return band radiance per µm [W m⁻² sr⁻¹ µm⁻¹] from radiance per cm⁻¹ [W m⁻² sr⁻¹ (cm⁻¹)⁻¹]; otherwise the band average is in the input's units. |
False
|
Returns:
| Type | Description |
|---|---|
BandInstrument
|
The instrument, |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the SRF is on a different grid. |
Examples:
A flat spectrum per µm, \(L_{\tilde\nu} = c \cdot 10^4 / \tilde\nu^2\),
reads c in every band:
>>> import numpy as np
>>> from radtrax.instrument import SpectralResponseFunction
>>> nu = np.linspace(800.0, 1000.0, 2001)
>>> srf = SpectralResponseFunction(
... wavelengths_hr_nm=1e7 / nu[::-1],
... band_centers_nm=np.array([10900.0, 11500.0]),
... band_widths_nm=np.array([900.0, 900.0]),
... band_names=("B10", "B11"),
... )
>>> band = BandInstrument.from_srf(srf, nu, per_um=True)
>>> [round(float(y), 12) for y in band(7.5 * 1e4 / nu**2)]
[7.5, 7.5]
Source code in src/radtrax/instrument/spectral.py
178 179 180 181 182 183 184 185 186 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 | |
GaussianInstrument
¶
GaussianInstrument(nu_cm: Float[ArrayLike, ' G'], center_nm: Float[ArrayLike, ' B'], fwhm_nm: Float[ArrayLike, ' B'], *, shift_cm: Float[ArrayLike, ''] = 0.0, shift_nm: Float[ArrayLike, ''] = 0.0, band_names: tuple[str, ...] | None = None)
Bases: Instrument
Gaussian bands built in JAX, shifted analytically in cm⁻¹ or in nm.
y_b is the trapezoidal average of the radiance over band b's
response f_b(ν) = exp(−(10⁷/(ν − δ) − δ_λ − c_b)² / 2σ_b²); see the
module docstring. Keep each band's response inside the grid: a band
shifted off it raises equinox.EquinoxRuntimeError (a RuntimeError).
Attributes:
| Name | Type | Description |
|---|---|---|
nu_cm |
Float[Array, ' G']
|
Wavenumber grid [cm⁻¹], strictly increasing, |
center_nm |
Float[Array, ' B']
|
Band centres [nm], |
fwhm_nm |
Float[Array, ' B']
|
Band FWHM [nm], |
shift_cm |
Float[Array, '']
|
Spectral shift |
shift_nm |
Float[Array, '']
|
Spectral shift |
band_names |
tuple[str, ...]
|
One label per band. |
Examples:
>>> import jax.numpy as jnp
>>> nu = jnp.linspace(4250.0, 4350.0, 1001)
>>> instrument = GaussianInstrument(
... nu, jnp.array([2325.0]), jnp.array([8.5])
... )
>>> round(float(instrument(jnp.ones(1001))[0]), 12) # a flat spectrum
1.0
Source code in src/radtrax/instrument/spectral.py
response
¶
The shifted, trapezoid-weighted response, each row summing to 1, (B, G).
Source code in src/radtrax/instrument/spectral.py
response_matrix
¶
from_srf
classmethod
¶
from_srf(srf: SpectralResponseFunction, nu_cm: Float[ArrayLike, ' G'], *, shift_cm: Float[ArrayLike, ''] = 0.0, shift_nm: Float[ArrayLike, ''] = 0.0) -> GaussianInstrument
The bands of a Gaussian :class:~radtrax.instrument.SpectralResponseFunction.
Raises:
| Type | Description |
|---|---|
ParameterError
|
If the SRF's bands are not Gaussian. |
Source code in src/radtrax/instrument/spectral.py
srf¶
Spectral Response Function — band integration of hyperspectral radiance.
The HAPI LUTs in :mod:radtrax.spectroscopy.hitran tabulate σ(ν, T, P) on a
fine line-by-line wavenumber grid. Real instruments observe band-integrated
radiance
L_b = ∫ f_b(λ) · L(λ) dλ / ∫ f_b(λ) dλ
where f_b(λ) is the instrument's Spectral Response Function for band b.
This module builds the (n_bands, n_λ) SRF matrix once — each row is a
normalised response profile — and applies it as a plain matrix multiplication
against a high-resolution radiance spectrum (1-D) or radiance cube (3-D).
The class mirrors the API in the Orbio Project Eucalyptus tutorial
(SpectralFilter + get_sensor_spectral_response_function) but exposes
a linear-operator view: apply (forward), jacobian (tangent action —
identical to forward since SRF is linear), and adjoint (Sᵀ for
reverse-mode / VJP use in 3DVar). These three methods match the operator
interface used by the observation-operator classes in the original
jej_vc_snippets/methane_retrieval/lut_obs_op.py.
Three parametric SRF shapes are supported:
'gaussian':exp[-(λ-λ_c)² / (2 σ²)]withFWHM = 2√(2 ln 2) σ.'rectangular': 1 inside[λ_c − W/2, λ_c + W/2], 0 outside.'triangular': linear ramp peaking atλ_c, zero at the edges.
Plus 'custom' which accepts an arbitrary (n_bands, n_λ) matrix
(each row gets normalised to sum to 1).
Ported from plumax 0.1.2 at 28f257e (src/plumax/radtran/srf.py); only import paths changed.
SpectralResponseFunction
dataclass
¶
SpectralResponseFunction(wavelengths_hr_nm: ndarray, band_centers_nm: ndarray, band_widths_nm: ndarray, band_names: tuple[str, ...] = ('B0',), srf_type: SRFType = 'gaussian', custom_srfs: ndarray | None = None)
Band-integration operator for a multispectral / hyperspectral instrument.
Attributes¶
wavelengths_hr_nm : np.ndarray
High-resolution wavelength grid [nm], shape (n_lambda,). Must be
monotone increasing.
band_centers_nm : np.ndarray
Band centre wavelengths [nm], shape (n_bands,).
band_widths_nm : np.ndarray
Band FWHM (for 'gaussian'), full-width (for 'rectangular'/'triangular')
[nm], shape (n_bands,).
band_names : tuple of str
Band labels, length n_bands.
srf_type : str
'gaussian' | 'rectangular' | 'triangular' | 'custom'.
custom_srfs : np.ndarray or None
Used only when srf_type == 'custom'. Shape (n_bands, n_lambda).
Notes¶
Rows of :attr:matrix are L1-normalised so that a flat input spectrum
L(λ) = c maps to L_b = c for every band; this matches the
"dot-product-in-frequency" convention in the Eucalyptus tutorial.
apply
¶
Forward operator: map HR spectrum/cube to band-integrated values.
Parameters¶
radiance_hr : np.ndarray
Input radiance. Shape (n_lambda,) for a single spectrum, or
(..., n_lambda) for a cube (spatial + spectral).
Returns¶
radiance_bands : np.ndarray
Band-integrated radiance with the final axis replaced by
n_bands.
Source code in src/radtrax/instrument/srf.py
jacobian
¶
Jacobian-vector product S · v. Linear operator → same as apply.
adjoint
¶
Adjoint operator Sᵀ: map band-space cotangents back to HR.
Used for reverse-mode / VJP gradient calculations.
Parameters¶
cotangent_bands : np.ndarray
Band-space cotangent, shape (..., n_bands).
Returns¶
cotangent_hr : np.ndarray
HR-space cotangent, shape (..., n_lambda).
Source code in src/radtrax/instrument/srf.py
check_srf_lut_coverage
¶
check_srf_lut_coverage(srf: SpectralResponseFunction, lut_wavelengths_nm: Float[ArrayLike, ' G'], *, context: str, tol: float = 0.01) -> Float[ndarray, ' B']
Warn when SRF bands respond outside the LUT's wavelength coverage.
Resampling a cross-section / target spectrum onto the SRF grid zero-fills
wavelengths beyond the LUT range (np.interp(..., left=0, right=0)),
silently attenuating any band whose response reaches there — a band whose
support lies wholly outside gets an exactly-zero (and misleading) value.
This computes each band's fraction of SRF mass outside the LUT coverage
and warns for bands exceeding tol.
Parameters¶
srf
Instrument SRF.
lut_wavelengths_nm
Wavelengths [nm] covered by the LUT (any order); coverage is taken as
[min, max].
context
Caller name, prepended to the warning message.
tol
Out-of-coverage SRF-mass fraction above which a band is flagged.
Returns¶
frac_out : np.ndarray Per-band fraction of SRF response outside the LUT coverage.
Source code in src/radtrax/instrument/srf.py
psf¶
JAX point-spread + ground-sampling instrument operators.
These two operators model the deterministic part of an imaging-spectrometer forward chain that sits between the radiative-transfer output and the band-integration step:
HR radiance cube ──PSF──► blurred HR cube ──GSD──► LR cube ──SRF──► bands
The :class:~radtrax.instrument.srf.SpectralResponseFunction already
covers the SRF stage; this module fills in the PSF (spatial blur) and GSD
(detector down-sampling). Both operators here are:
- JAX-native — every method is
jit-able andvmap-able; gradients flow throughapply/jacobian/adjointvia reverse-mode AD without manual bookkeeping. This is the prerequisite for the variational retrieval module :mod:radtrax.retrieval.var3d. - Linear —
jacobian == apply(state-independent), andadjointimplements the Euclidean transpose so the inner-product identity⟨A x, y⟩ = ⟨x, Aᵀ y⟩holds to machine precision. - Cube-shaped (band-last) — inputs are
(ny, nx, n_lambda); channels are processed independently. This matches the convention in :mod:radtrax.instrument.srfand plays nicely withjax.vmapalong the spectral axis.
The numpy reference for the PSF/GSD logic lives in
jej_vc_snippets/methane_retrieval/lut_obs_op.py; this is the JAX rewrite
that lets the same operators participate in autodiff-based 3D-Var solves.
Both operators are Equinox modules (radtrax#57): the PSF kernel is a pytree
leaf, so jax.grad reaches it, jax.vmap maps over a batch of PSFs, and
eqx.filter_jit compiles once per kernel shape rather than once per
instance.
Ported from plumax 0.1.2 at 28f257e (src/plumax/radtran/instrument.py),
where both were frozen dataclasses.
PointSpreadFunction
¶
Bases: Module
2D PSF blur applied independently per spectral channel.
The PSF is a fixed convolution kernel (kernel_size × kernel_size,
L1-normalised) so the operator is linear and shift-invariant. The
Jacobian is the operator itself; the adjoint is convolution with the
180°-flipped kernel (real-valued case).
The kernel is a pytree leaf: differentiate through it with
eqx.filter_grad, map over a stack of PSFs with eqx.filter_vmap.
Unlike the plumax dataclass, an instance is not hashable (its kernel is an
array), so it cannot be a static jit argument or a dict key; pass it
to eqx.filter_jit instead.
A kernel that sums to zero raises
DegenerateInputError; a traced kernel is
checked with eqx.error_if instead, which raises a RuntimeError at
run time.
Attributes¶
kernel : Array
L1-normalised 2-D kernel, shape (kernel_size, kernel_size).
Source code in src/radtrax/instrument/psf.py
normalised_kernel
property
¶
The kernel divided by its sum, as apply and adjoint use it.
__init__ stores a normalised kernel, but an update that skips
__init__ (an optimiser step, eqx.tree_at) need not keep the sum
at 1; normalising at use keeps the blur radiance-conserving and makes
the kernel's gradient tangent to the Σ K = 1 constraint.
gaussian
classmethod
¶
gaussian(fwhm_pixels: float, kernel_size: int = 9) -> PointSpreadFunction
Build an isotropic Gaussian PSF.
FWHM = 2√(2 ln 2) σ; the kernel is sampled on the discrete pixel grid
and L1-normalised in __init__.
Source code in src/radtrax/instrument/psf.py
airy
classmethod
¶
airy(fwhm_pixels: float, kernel_size: int = 9) -> PointSpreadFunction
Build an Airy-disk-like PSF as a diffraction-ish surrogate.
fwhm_pixels is the kernel's full width at half maximum, matching
:meth:gaussian. The Airy argument is scaled so the half-max radius
(x = _AIRY_HALF_MAX_X) falls at r = fwhm_pixels / 2; the earlier
1.22·fwhm (first-zero) scaling produced a kernel ≈ 0.63× the
requested FWHM.
Source code in src/radtrax/instrument/psf.py
apply
¶
Forward blur: y = K * x per channel.
Source code in src/radtrax/instrument/psf.py
jacobian
¶
adjoint
¶
Adjoint: convolution with the flipped kernel.
For a real-valued L1-normalised kernel K the discrete adjoint of
the per-channel cross-correlation y = K ⋆ x is Kᵀ ⋆ u = K[::-1] ⋆ u.
Symmetric kernels (e.g. Gaussian) are self-adjoint.
Source code in src/radtrax/instrument/psf.py
GroundSamplingDistance
¶
Bases: Module
Block-mean down-sampling operator for a fixed integer factor.
Maps an HR cube (ny, nx, n_lambda) to LR
(ny // f, nx // f, n_lambda) by averaging non-overlapping f × f
spatial blocks. If (ny, nx) is not divisible by f, the input is
truncated to (ny // f * f, nx // f * f) (top-left crop).
The adjoint distributes each LR value uniformly across the corresponding
HR block with weight 1 / f² so that ⟨A x, y⟩ = ⟨x, Aᵀ y⟩ holds
exactly under the standard Euclidean inner product.
Attributes¶
downsample_factor : int
Integer block size f. Static: it fixes the output shape, so a new
factor is a new compiled function.
Notes¶
The classmethod :meth:from_optics derives f from the standard
pinhole-camera GSD formula
GSD [m/px] = sensor_width [mm] · altitude [m] / (focal_length [mm] · image_width [px])
and rounds to the nearest integer. For non-integer ratios you should
pre-resample the HR cube — block-mean is only well-defined for integer
factors.
from_optics
classmethod
¶
from_optics(sensor_width_mm: float, focal_length_mm: float, image_width_px: int, altitude_m: float, pixel_size_hr_m: float) -> GroundSamplingDistance
Build a GSD operator from camera + altitude + HR pixel size.
Dimensional analysis:
GSD [m/px] = (sensor_pixel_size [m/px]) · (altitude [m]) / (focal_length [m])
= (sensor_width_mm / image_width_px) · altitude_m / focal_length_mm
The mm units on numerator and denominator cancel, so no unit-conversion
factor is needed — the numpy reference snippet we ported from had a spurious
× 1000 that inflated every realistic camera's GSD by a factor of 1000.
Source code in src/radtrax/instrument/psf.py
lr_shape
¶
LR shape implied by an HR shape and the current factor.
Source code in src/radtrax/instrument/psf.py
apply
¶
Forward down-sample by block mean.
Source code in src/radtrax/instrument/psf.py
jacobian
¶
adjoint
¶
Adjoint: spread each LR value uniformly across its HR block.
Parameters¶
cotangent : Array
LR cotangent, shape (ny_lr, nx_lr, n_lambda).
hr_shape : tuple
Target HR shape (ny_hr, nx_hr, n_lambda). Truncated entries
from :meth:apply (when ny_hr or nx_hr are not multiples
of f) are filled with zeros — the adjoint of the truncation
is the inclusion (... → ny_lr·f rows).
Source code in src/radtrax/instrument/psf.py
noise¶
The NoiseModel protocol: the measurement-noise covariance S_ε.
Optimal estimation needs S_ε as a structured operator, and synthetic
scenes need draws from it. A noise model returns S_ε(y) for the band
values y it is evaluated at (signal-dependent noise needs y), as a
lineax operator tagged positive semidefinite (so gx.solve, gx.logdet
and lx.CG all take it), draws ε ~ 𝒩(0, S_ε(y)) from a
jax.random key the caller passes, and scores an observation with
log 𝒩(y_obs | y, S_ε(y)).
Three diagonal models:
DiagonalNoise: a fixed per-band standard deviation,S_ε = diag(σ²).SNRNoise: a signal-to-noise ratio with a noise floor,σ_b = √((y_b / SNR_b)² + σ_floor,b²).NEdTNoise: a thermal band's noise-equivalent temperature difference turned into radiance,σ_b = NEΔT_b · ∂B̄_b/∂Tat a reference temperature or at the scene's band brightness temperature (radtrax#249).
Shapes: B bands.
NoiseModel
¶
Bases: Module
Protocol: the measurement-noise covariance S_ε(y) and draws from it.
An abstract Equinox module, so filter_jit compiles each concrete
model and a retrieval can take any of them. A subclass implements
covariance; sample draws through gx.sample_mvn, which
dispatches on the operator's structure (√σ² · z for a diagonal).
covariance
abstractmethod
¶
S_ε at the band values y, a positive-definite (B, B) operator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Float[ArrayLike, ' B']
|
Band values the noise is evaluated at, |
required |
Source code in src/radtrax/instrument/noise.py
sample
¶
One draw ε ~ 𝒩(0, S_ε(y)); add it to y for a noisy observation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
PRNGKeyArray
|
A |
required |
y
|
Float[ArrayLike, ' B']
|
Band values the noise is evaluated at, |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' B']
|
The noise |
Source code in src/radtrax/instrument/noise.py
log_prob
¶
log 𝒩(y_obs | y_model, S_ε(y_model)), through gx.gaussian_log_prob.
The noise is evaluated at the modelled values, so for signal-dependent
noise the log |S_ε| term depends on them too and is kept: dropping
it would favour brighter solutions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_obs
|
Float[ArrayLike, ' B']
|
Observed band values, |
required |
y_model
|
Float[ArrayLike, ' B']
|
Modelled band values, |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
The log-density, a scalar. |
Source code in src/radtrax/instrument/noise.py
DiagonalNoise
¶
Bases: NoiseModel
Independent noise with a fixed standard deviation per band, S_ε = diag(σ²).
Attributes:
| Name | Type | Description |
|---|---|---|
sigma |
Float[Array, ' B']
|
Standard deviation per band, in the units of |
Examples:
>>> import jax.numpy as jnp
>>> noise = DiagonalNoise(jnp.array([0.1, 0.2]))
>>> import gaussx as gx
>>> [round(v, 6) for v in gx.diag(noise.covariance(jnp.ones(2))).tolist()]
[0.01, 0.04]
Source code in src/radtrax/instrument/noise.py
covariance
¶
diag(σ²), whatever y; y sets only the band count.
SNRNoise
¶
Bases: NoiseModel
Signal-dependent noise from a signal-to-noise ratio and a floor.
σ_b = √((y_b / SNR_b)² + σ_floor,b²), S_ε = diag(σ²): the floor
keeps S_ε positive definite where the signal vanishes.
Attributes:
| Name | Type | Description |
|---|---|---|
snr |
Float[Array, ' B']
|
Signal-to-noise ratio per band (dimensionless), |
floor |
Float[Array, ' B']
|
Noise floor per band, in the units of |
Examples:
>>> import jax.numpy as jnp
>>> noise = SNRNoise(snr=jnp.array([100.0]), floor=jnp.array([0.03]))
>>> import gaussx as gx
>>> round(float(gx.diag(noise.covariance(jnp.array([4.0])))[0]), 6)
0.0025
Source code in src/radtrax/instrument/noise.py
covariance
¶
diag((y / SNR)² + σ_floor²), differentiable in y.
Source code in src/radtrax/instrument/noise.py
NEdTNoise
¶
NEdTNoise(netd_k: Real[ArrayLike, ' B'], instrument: Instrument, *, t_ref_k: Real[ArrayLike, ''] = 300.0, mode: Literal['reference', 'scene'] = 'reference')
Bases: NoiseModel
Thermal-band noise from a noise-equivalent temperature difference.
with \(A\) the instrument's response matrix and \(B\) the Planck radiance
(radtrax.radiometry.band_planck_radiance). Two conventions for \(T\):
"reference"(the default): \(T = T_\mathrm{ref}\), the temperature the specification quotes NEΔT at (300 K for TIRS-2 and ECOSTRESS). \(σ\) is constant iny, as for a detector-noise-limited instrument; \(\partial \bar B / \partial T\) at a scalar \(T_\mathrm{ref}\) is one JVP."scene": \(T = T_b(y)\), each band's brightness temperature ofy(radtrax.radiometry.band_brightness_temperature), for "NEΔT at the scene temperature" taken literally. \(σ\) depends ony, solog_probkeeps \(\log |S_ε|\) (theNoiseModelcontract), and a band with \(y_b \le 0\) has no brightness temperature: its \(σ\) is NaN.
\(σ\) is recomputed in every call (not cached at construction), so gradients reach the instrument's parameters; it is in the instrument's radiance units (W m⁻² sr⁻¹ (cm⁻¹)⁻¹ for rows that sum to 1).
Attributes:
| Name | Type | Description |
|---|---|---|
netd_k |
Float[Array, ' B']
|
NEΔT per band [K], |
instrument |
Instrument
|
The bands \(\bar B\) is integrated over. |
t_ref_k |
Float[Array, '']
|
Reference temperature [K], a scalar > 0 (used in the
|
mode |
Literal['reference', 'scene']
|
|
Examples:
>>> import jax.numpy as jnp
>>> from radtrax.instrument import GaussianInstrument
>>> nu = jnp.linspace(916.0, 919.5, 3501) # a 1 nm band at 10.9 µm
>>> band = GaussianInstrument(nu, jnp.array([10900.0]), jnp.array([1.0]))
>>> noise = NEdTNoise(jnp.array([0.048]), band)
>>> f"{float(noise.radiance_sigma(jnp.zeros(1))[0]):.4e}"
'8.1485e-05'
Source code in src/radtrax/instrument/noise.py
radiance_sigma
¶
\(σ_b = \mathrm{NEΔT}_b \, \partial \bar B_b / \partial T\), in the instrument's units.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Float[ArrayLike, ' B']
|
Band values the noise is evaluated at, |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' B']
|
\(σ\), |
Source code in src/radtrax/instrument/noise.py
netd_at
¶
The NEΔT this noise amounts to at the scene: \(σ_b / \partial \bar B_b / \partial T|_{T_b(y)}\) [K].
The same radiance noise is a different NEΔT at another scene
temperature: in the "reference" mode it equals netd_k only at
\(y = \bar B(T_\mathrm{ref})\); in the "scene" mode it is
netd_k wherever \(y > 0\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Float[ArrayLike, ' B']
|
Band values, |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' B']
|
NEΔT per band [K], |
Source code in src/radtrax/instrument/noise.py
covariance
¶
diag(σ²) with \(σ\) from radiance_sigma, differentiable in the instrument.