Slab models¶
plumax's single-slab Beer–Lambert models (exact, Maclaurin, Taylor) with hand-written Jacobians, the normalised-brightness table, and the observation configuration. Kept as the regression oracle for the layered v0 forward model.
slab
¶
plumax's single-slab Beer–Lambert models, kept as the regression oracle.
One homogeneous slab at one (p, T): forward holds the exact, Maclaurin and
Taylor forward models with their hand-written Jacobians, nb_lut the
normalised-brightness table for fast per-pixel plume injection, and
config the observation geometry, instrument spec and unit constants. The
layered v0 forward model (Wave 3) must reproduce these at L = 1 (gate V1).
Ported from plumax 0.1.2 at 28f257e (plumax.radtran, the forward half; its
matched-filter half was retired in favour of radtrax.retrieval.matched_filter).
config¶
Observation-geometry and instrument configuration for radtran.
The forward models in :mod:radtrax.slab.forward need three
groups of inputs: atmospheric state (T, P, background VMR), a
gas-species LUT (σ(ν, T, P), from :mod:radtrax.spectroscopy.hitran), and
the viewing geometry and instrument response.
This module factors the latter two into frozen dataclasses:
- :class:
ObservationGeometry— solar/viewing zenith angles, vertical path length, optional AMF override. - :class:
InstrumentSpec— band centres and widths for a synthetic multispectral instrument. Real instruments should build a :class:~radtrax.instrument.srf.SpectralResponseFunctionfrom measured SRFs;InstrumentSpecis a convenient constructor for the tutorial notebooks.
The constants, the number density and the air-mass factor have one home,
:mod:radtrax.atmosphere (radtrax#27); this module re-exports the
constants and wraps the number density with scalar validation.
Ported from plumax 0.1.2 at 28f257e (src/plumax/radtran/config.py); only import paths changed.
ObservationGeometry
dataclass
¶
ObservationGeometry(sza_deg: float = 30.0, vza_deg: float = 0.0, path_length_cm: float = 840000.0, amf: float | None = None)
Viewing geometry for a single pixel.
Attributes¶
sza_deg : float
Solar zenith angle [degrees], 0 (overhead sun) to ~90 (grazing).
vza_deg : float
Viewing zenith angle [degrees], 0 (nadir) to ~90 (limb).
path_length_cm : float
Vertical atmospheric path [cm] traversed by the line of sight before
AMF correction. For a single-slab column over the full atmosphere,
a common choice is L = p_surf / (ρ · g) ≈ 8.4 km = 8.4e5 cm
(scale height of dry air at 1 atm, 288 K).
amf : float, optional
Air-mass factor. If None the plane-parallel two-way form
1/cos(SZA) + 1/cos(VZA) is used.
Notes¶
The plane-parallel AMF breaks down past ~75° zenith; beyond that a spherical correction (Chapman function) is needed. For the tutorial regime (SZA, VZA < 60°) the plane-parallel form is within a few percent of the exact value.
air_mass_factor
property
¶
Plane-parallel two-way AMF, or the explicit override if provided.
InstrumentSpec
dataclass
¶
InstrumentSpec(name: str = 'synthetic', band_centers_nm: ndarray = (lambda: array([1610.0, 2190.0], dtype=float))(), band_widths_nm: ndarray = (lambda: array([90.0, 180.0], dtype=float))(), band_names: tuple[str, ...] = ('B11', 'B12'), srf_type: str = 'gaussian')
Multispectral instrument description.
Attributes¶
name : str
Free-text instrument identifier for dataset metadata.
band_centers_nm : np.ndarray
Band centre wavelengths [nm], shape (n_bands,).
band_widths_nm : np.ndarray
Full-width at half-maximum of each band [nm], shape (n_bands,).
band_names : tuple of str
Short names for each band (e.g. ('B11', 'B12')). Length matches
band_centers_nm.
srf_type : str
Shape of the synthetic SRF built by
:meth:InstrumentSpec.make_srf — one of 'gaussian',
'rectangular', 'triangular'.
Examples¶
Sentinel-2 B11/B12-like synthetic instrument::
InstrumentSpec(
name="S2A-like",
band_centers_nm=np.array([1610.0, 2190.0]),
band_widths_nm=np.array([90.0, 180.0]),
band_names=("B11", "B12"),
)
make_srf
¶
make_srf(wavelengths_nm: Float[ArrayLike, ' Ghr']) -> SpectralResponseFunction
Construct a :class:SpectralResponseFunction over wavelengths_nm.
Imported locally to avoid a circular import at module load.
Source code in src/radtrax/slab/config.py
number_density_cm3
¶
Ideal-gas total number density [molecules / cm³].
Source code in src/radtrax/slab/config.py
forward¶
Beer-Lambert forward models for atmospheric retrieval.
Three flavours, each returning radiance + Jacobian + transmittance:
- :func:
forward_nonlinear— exact Beer-LambertL = L₀ · exp(-τ). - :func:
forward_maclaurin— polynomial inVMRvia Maclaurin ofexp(-τ)aroundVMR = 0. Order 1 is the classical "linear" retrieval. - :func:
forward_taylor— Taylor-linearised around a background stateVMR_bg(the state used in 3D-/4D-Var inner loops).
Plus normalised variants that divide by the background radiance:
- :func:
forward_nonlinear_normalized—L_norm = exp(-Δτ); cancels surface reflectance, solar irradiance, and common aerosol slope. - :func:
forward_maclaurin_normalized/ :func:forward_taylor_normalized.
All functions take an xarray.Dataset LUT carrying an
absorption_cross_section variable with dims (wavenumber, temperature,
pressure) — the output of
:func:radtrax.spectroscopy.hitran.build_lut_dataset.
Ported and adapted from
jej_vc_snippets/methane_retrieval/lut_model_beers.py — the Jacobian is
now always returned alongside the radiance so the same functions can drive
variational retrievals (H = dL/dVMR) and matched-filter target-spectrum
construction (t = H · ΔVMR).
Ported from plumax 0.1.2 at 28f257e (src/plumax/radtran/forward.py); only import paths changed.
ForwardResult
dataclass
¶
Bundle of (radiance, Jacobian, transmittance) returned by the forward models.
Using a frozen dataclass rather than a plain tuple gives the caller
named access — result.jacobian — which matters because target
spectra for the matched filter (:mod:radtrax.retrieval.matched_filter)
consume only the Jacobian or only the transmittance.
Attributes¶
radiance : np.ndarray
Simulated radiance at each wavenumber, shape (n_nu,).
jacobian : np.ndarray
dL/dVMR evaluated at the supplied state, shape (n_nu,).
transmittance : np.ndarray
Atmospheric transmittance exp(-τ) (non-normalised variants) or
exp(-Δτ) (normalised variants), shape (n_nu,).
forward_nonlinear
¶
forward_nonlinear(ds: Dataset, nu_obs: Float[ArrayLike, ' G'], *, T_K: float, p_atm: float, vmr: float, path_length_cm: float, amf: float, surface_reflectance: float = 1.0, solar_irradiance: float = 1.0, var: str = 'absorption_cross_section') -> ForwardResult
Exact Beer-Lambert forward model.
Radiance: L(ν) = (F₀ R / π) · exp(-τ(ν, VMR))
Optical τ: τ = σ · N_total · VMR · L · AMF
Jacobian: dL/dVMR = -L · (dτ/dVMR), dτ/dVMR = σ · N_total · L · AMF
Defaults surface_reflectance = solar_irradiance = 1 collapse the
prefactor to 1/π, which is convenient for toy tests where the
absolute radiance scale does not matter.
Source code in src/radtrax/slab/forward.py
forward_nonlinear_normalized
¶
forward_nonlinear_normalized(ds: Dataset, nu_obs: Float[ArrayLike, ' G'], *, T_K: float, p_atm: float, vmr_background: float, delta_vmr: float, path_length_cm: float, amf: float, var: str = 'absorption_cross_section') -> ForwardResult
Exact Beer-Lambert normalised by the background radiance.
L_norm = L(VMR_bg + ΔVMR) / L(VMR_bg) = exp(-Δτ), which cancels any
multiplicative surface/solar/aerosol factors that appear equally in the
plume and background pixels.
Returns¶
ForwardResult
radiance is the normalised transmittance exp(-Δτ);
transmittance is the same quantity (kept for API symmetry with
:func:forward_nonlinear); jacobian is
d(L_norm)/d(ΔVMR) = -exp(-Δτ) · (σ · N · L · AMF).
Source code in src/radtrax/slab/forward.py
forward_maclaurin
¶
forward_maclaurin(ds: Dataset, nu_obs: Float[ArrayLike, ' G'], *, T_K: float, p_atm: float, vmr: float, path_length_cm: float, amf: float, surface_reflectance: float = 1.0, solar_irradiance: float = 1.0, order: int = 1, var: str = 'absorption_cross_section') -> ForwardResult
Maclaurin-series forward model: expand exp(-τ(VMR)) around VMR = 0.
With a = σ · N · L · AMF so τ = a · VMR:
- order 1:
T ≈ 1 − a·VMR(linear in VMR). - order 2:
T ≈ 1 − a·VMR + ½ (a·VMR)². - order 3:
T ≈ 1 − a·VMR + ½ (a·VMR)² − ⅙ (a·VMR)³.
Accurate when the total optical depth a·VMR is ≪ 1 — the regime of
classical linear retrievals for thin absorbers.
Source code in src/radtrax/slab/forward.py
forward_maclaurin_normalized
¶
forward_maclaurin_normalized(ds: Dataset, nu_obs: Float[ArrayLike, ' G'], *, T_K: float, p_atm: float, delta_vmr: float, path_length_cm: float, amf: float, order: int = 1, var: str = 'absorption_cross_section') -> ForwardResult
Maclaurin expansion of exp(-Δτ) around ΔVMR = 0.
At order 1 this reduces to L_norm ≈ 1 − σ · N · ΔVMR · L · AMF — the
canonical linearised retrieval signal.
Source code in src/radtrax/slab/forward.py
forward_taylor
¶
forward_taylor(ds: Dataset, nu_obs: Float[ArrayLike, ' G'], *, T_K: float, p_atm: float, vmr: float, vmr_background: float, path_length_cm: float, amf: float, surface_reflectance: float = 1.0, solar_irradiance: float = 1.0, var: str = 'absorption_cross_section') -> ForwardResult
First-order Taylor expansion around vmr_background.
L(VMR) ≈ L_b + H · (VMR − VMR_b) with H = dL/dVMR |_{VMR_b}. This
is the linearisation used in the inner loops of 3D-/4D-Var.
Source code in src/radtrax/slab/forward.py
forward_taylor_normalized
¶
forward_taylor_normalized(ds: Dataset, nu_obs: Float[ArrayLike, ' G'], *, T_K: float, p_atm: float, delta_vmr: float, path_length_cm: float, amf: float, var: str = 'absorption_cross_section') -> ForwardResult
Taylor expansion of L_norm around ΔVMR = 0.
Since L_norm(0) = 1 and dL_norm/dΔVMR(0) = -σ · N · L · AMF,
the first-order Taylor expansion coincides with the order-1 Maclaurin
expansion. Kept as a named function for API symmetry with
:func:forward_taylor.
Source code in src/radtrax/slab/forward.py
nb_lut¶
Normalised-brightness LUT for fast per-pixel plume injection.
The band-integrated normalised brightness for a pixel with methane column
enhancement ΔX over a background column X_bg is
nB_b(ΔX) = T_b(X_bg + ΔX) / T_b(X_bg),
T_b(X) = ∫ f_b(λ) · exp(-AMF · σ(λ) · X) dλ / ∫ f_b(λ) dλ
where f_b is the instrument SRF for band b and σ is the target
gas cross-section in cm²/molecule. This integral is the natural scalar
"plume throughput" for band b once surface reflectance and solar
irradiance have been divided out (differential Beer-Lambert).
The background matters: it has already darkened the strongest line cores,
so a plume over it absorbs less than in clean air. plumax built the table
with X_bg = 0, which overstates Sentinel-2 band 12's plume optical depth
by 1.1 % (radtrax · 2, the GeoStack design note); pass the scene's
background column as background_column (radtrax#36). For methane at
1.9 ppm over a 1013 hPa column it is 0.678 mol/m² (0.0109 kg/m²). From a
profile, AtmosphericState.column(gas) is in molecules/cm²: divide by
AVOGADRO_PER_MOL and multiply by CM2_PER_M2 for mol/m².
Evaluating the integral per pixel is expensive, so we follow the Eucalyptus
radtran() trick: pre-tabulate nB over a 1-D grid of ΔX values, then
look up per-pixel via piecewise-linear interpolation (np.interp). The
grid is quadratic, ΔX_k = ΔX_max (k / (n − 1))²: the interpolation error
of a small plume is first order in the step near 0, where the band optical
depth curves most, so plumax's uniform grid of 4001 points over [0, 200]
mol/m² missed the exact band integral by up to 1.4 % at small ΔX on
radtrax · 3's synthetic table, and the quadratic one by 4e-6 (gate V5,
radtrax#36). Plume injection is a single
np.interp call per band.
Public surface¶
- :func:
build_nb_lut— build the(n_bands, n_delta)LUT. - :func:
lookup_nb— look up nB per band for a 2-D plume map. - :func:
inject_plume— apply nB to a clean multispectral scene.
Ported from plumax 0.1.2 at 28f257e (src/plumax/radtran/nb_lut.py); the
background column is radtrax's (radtrax#36).
NBLookup
dataclass
¶
NBLookup(delta_column: ndarray, nB: ndarray, band_names: tuple[str, ...], background_column: float = 0.0)
Pre-computed normalised-brightness table, indexed by band and ΔX.
Attributes¶
delta_column : np.ndarray
Column-enhancement grid [mol/m²], shape (n_delta,). Strictly
increasing, starts at 0 (quadratic for :func:build_nb_lut).
nB : np.ndarray
Normalised brightness per band at each grid point, shape
(n_bands, n_delta). nB[:, 0] == 1 always.
band_names : tuple of str
Band labels, length n_bands.
background_column : float
Background column X_bg the table is relative to [mol/m²];
0 for plumax's clean-air table.
to_dataset
¶
Wrap as an :class:xarray.Dataset for easy serialisation.
Source code in src/radtrax/slab/nb_lut.py
build_nb_lut
¶
build_nb_lut(ds: Dataset, srf: SpectralResponseFunction, *, T_K: float, p_atm: float, amf: float, max_delta_column: float = 200.0, n_grid: int = 4001, var: str = 'absorption_cross_section', background_column: float = 0.0) -> NBLookup
Pre-tabulate band-integrated nB over a column-enhancement grid.
Parameters¶
ds : xr.Dataset
HAPI absorption-cross-section LUT, output of
:func:radtrax.spectroscopy.hitran.build_lut_dataset.
srf : SpectralResponseFunction
Instrument SRF mapping the LUT wavelength grid to bands.
T_K, p_atm : float
Slab atmospheric temperature [K] and pressure [atm], at which the
cross-section σ(λ; T, P) is interpolated. ΔX is a column
already, so the slab path length does not enter the integrand.
amf : float
Two-way air-mass factor multiplying the slant column.
max_delta_column : float
Upper bound of the ΔX grid [mol/m²]. Default 200.
n_grid : int
Number of grid points on the quadratic grid (default 4001: steps
from 1.2e-5 mol/m² at 0 to 0.1 mol/m² at 200).
var : str
Cross-section variable name (default 'absorption_cross_section',
matching :func:radtrax.spectroscopy.hitran.build_lut_dataset).
background_column : float
Background column X_bg [mol/m², not molecules/cm²] the plume
sits on; nB is relative to it. The default, 0, is plumax's
clean-air table, which overstates a strong band's plume optical
depth (module docstring).
Returns¶
NBLookup
nB[b, k] is the band-integrated normalised brightness at
delta_column[k] for band b.
Source code in src/radtrax/slab/nb_lut.py
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 177 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 | |
lookup_nb
¶
lookup_nb(plume_column: Float[ArrayLike, '*shape'], nb_lookup: NBLookup) -> Float[ndarray, ' B *shape']
Look up per-band nB for every pixel of a 2-D column map.
Values outside the LUT range are clipped. Clipping at the top end is physically conservative (saturates the absorption); negative inputs are clipped to zero since they are not physical.
Parameters¶
plume_column : np.ndarray
Column enhancement [mol/m²], shape (ny, nx) or any broadcastable
shape. NaNs are preserved as NaNs in the output.
nb_lookup : NBLookup
Pre-built LUT.
Returns¶
nb_map : np.ndarray
Shape (n_bands, *plume_column.shape). Dimensionless multiplier
in [0, 1] (1 at ΔX=0).
Source code in src/radtrax/slab/nb_lut.py
inject_plume
¶
inject_plume(scene: Float[ArrayLike, '*scene'], plume_column: Float[ArrayLike, 'H W'], nb_lookup: NBLookup, *, band_axis: int = 0) -> Float[ndarray, '*scene']
Multiplicative plume injection: scene_plume = scene · nB(ΔX).
Parameters¶
scene : np.ndarray
Clean multispectral scene, shape (n_bands, ny, nx) by default
(band_axis=0).
plume_column : np.ndarray
ΔX column map [mol/m²], shape (ny, nx).
nb_lookup : NBLookup
Pre-built LUT. nb_lookup.n_bands must match scene.shape[band_axis].
band_axis : int
Axis of scene corresponding to bands. Default 0.
Returns¶
scene_plume : np.ndarray
Same shape as scene.