Coupled inversion¶
End-to-end coupled, multi-instrument source inversion. Tier IV.
coupled
¶
Tier IV — end-to-end coupled, multi-instrument source inversion.
Tier IV is assembly + multi-instrument fusion, not new physics (design Tier IV): it composes a transport tier with the observation operator (averaging kernel / RTM) and fuses several satellites' observations into one coherent posterior.
This v1 implements the design's build-order step 1 — Tier I (Gaussian plume) + averaging kernel + multi-instrument L2 fusion for a static emission rate, with the per-instrument bias as a first-class state element:
instrument— :class:Instrument: per-satellite receptor geometry, averaging kernel, andR = R_retr + R_repr + R_alignerror budget.forward— the coupled Tier I → column → AK forward, kept per-instrument at native resolution (build_coupled_forward/CoupledForward); linear in the emission rateQ.fusion— the closed-form joint posterior over(Q, bias_inst)(fuse_observations), exploiting that linearity (the design's linear-conditional-Gaussian limit).-
rtm— additive RTM-based observation operators that map the plume column enhancement → gasΔVMR→ band-integrated normalised radiance via the :mod:radtrax.slabBeer–Lambert + SRF stack, as the build-order step toward L1-radiance fusion. Two complementary, well-known models: the exact nonlinearRadianceObservationOperator/radiance_response(L_norm = exp(-Δτ)), and its tangent-linear counterpartLinearisedRadianceOperator/radiance_response_linear(vialinearise), which is linear inQand pure-JAX so it feeds gradient -based and closed-form inversion.LayeredRadianceOperatormaps the column through radtrax's layeredForwardModelinstead of the slab, in JAX end to end, sojax.gradwith respect toQruns through plume and radiance together. The scalar-AK path above is unchanged. -
backends— the Tier II and Tier III transports behind the same forward (:class:TransportBackend, :class:LagrangianSource, :class:EulerianSource), through the shared column observation. -
quality— the per-instrument quality-flag aggregator (:class:QualityPolicy, :func:apply_quality, :func:contribution_report): flagged pixels are masked or have their variance inflated, and :func:stack_observationshands the result to vardax. -
inversion— the nonlinear end-to-end inversion (:func:solve_coupled) over the emission, source position, wind, background and biases, through the averaging kernel or a radiance operator per instrument: plumax builds the forward, prior and masks, vardax solves (ThreeDVar, L-BFGS) and gives the Laplace posterior. Needs theassimextra. -
k_sources— masked-K multi-source handling across instruments (:class:MaskedKPrior, :func:infer_masked_k_sources, :func:build_masked_k_problem, :func:saturation_rate), on Tier I's slot prior and posterior carrier.
Later build-order steps (the Q(t) stochastic
process, coupled emulator, operational
predictor) are future work; the fusion harness and likelihood structure here
are designed to stay the same as those blocks are swapped in.
EulerianSource
dataclass
¶
Tier III transport: the les_fvm finite-volume run through the shared column.
r_i = A_i Σ_k Δz c_k(x_i, t_inst; Q = 1),
the field of a unit constant emission at the overpass time t_inst, read in
the T-cell that holds each receptor. The forward is linear in the emission
with a zero initial field, so Q · r is the run at constant emission Q.
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
EulerianForward4DVar
|
The differentiable transport ( |
background |
float
|
Additive background column c_bg. |
unit_history
cached
property
¶
Interior field history of a unit constant emission, one solve.
time_index
¶
Index of the save time that equals the instrument's overpass time.
Raises:
| Type | Description |
|---|---|
GridMismatchError
|
If no save time matches it. |
Source code in src/plumax/coupled/_src/backends.py
column_response
¶
Column per unit emission at the instrument's receptors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Instrument
|
The observing instrument. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' obs']
|
The response r_inst. |
Raises:
| Type | Description |
|---|---|
GridMismatchError
|
If the overpass time is not a save time. |
DomainError
|
If a receptor lies outside the grid. |
Source code in src/plumax/coupled/_src/backends.py
LagrangianSource
dataclass
¶
LagrangianSource(emission_pattern: Float[Array, ' cell'] | Float[Array, 'cell param'], winds: tuple[GriddedWind | Callable[[Array], Array], ...], turbulence: TurbulenceModel, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], column_z: Float[Array, ' level'], n_particles: int = 1000, t_back: float = 600.0, dt: float = 1.0, pbl_height: float = 1000.0, pbl_fraction: float = 0.5, air_density: float = 1.2, seed: int = 0, background: float = 0.0)
Tier II transport: met-ensemble backward footprints through the shared column.
c_k(x_i) = ρ_air F̄(x_i, z_k) q, F̄ = mean_e F_e, r_i = A_i Σ_k h_k c_k(x_i),
with F_e the footprint of member e at receptor (x_i, z_k), q the surface
flux of a unit source and h_k the trapezoid weights over column_z.
Attributes:
| Name | Type | Description |
|---|---|---|
emission_pattern |
Float[Array, ' cell'] | Float[Array, 'cell param']
|
Surface flux per unit emission q [m⁻²] on the
footprint grid, cells flattened row-major over |
winds |
tuple[GriddedWind | Callable[[Array], Array], ...]
|
One mean wind per met-ensemble member (a |
turbulence |
TurbulenceModel
|
Turbulence model shared by the members. |
domain_x |
tuple[float, float, int]
|
|
domain_y |
tuple[float, float, int]
|
|
column_z |
Float[Array, ' level']
|
Receptor levels z_k [m] of the column, strictly increasing. |
n_particles |
int
|
Particles per receptor level and member. |
t_back |
float
|
Backward integration time [s]. |
dt |
float
|
Particle time step [s]. |
pbl_height |
float
|
Boundary-layer height [m]. |
pbl_fraction |
float
|
Fraction of the boundary layer that sees the surface flux. |
air_density |
float
|
Air density ρ_air [kg m⁻³]. |
seed |
int
|
Base PRNG seed of the particle draws. |
background |
float
|
Additive background column c_bg. |
point
classmethod
¶
point(location: tuple[float, float], *, winds: Sequence[GriddedWind | Callable[[Array], Array]], turbulence: TurbulenceModel, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], column_z: Float[Array, ' level'], **settings: float | int) -> LagrangianSource
A point source: unit emission spread over the cell that holds it.
q_s = 1 / A_cell in the containing cell s, zero elsewhere.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
location
|
tuple[float, float]
|
Source |
required |
winds
|
Sequence[GriddedWind | Callable[[Array], Array]]
|
One mean wind per met member. |
required |
turbulence
|
TurbulenceModel
|
Turbulence model. |
required |
domain_x
|
tuple[float, float, int]
|
|
required |
domain_y
|
tuple[float, float, int]
|
|
required |
column_z
|
Float[Array, ' level']
|
Receptor levels z_k [m]. |
required |
**settings
|
float | int
|
Any other field of :class: |
{}
|
Returns:
| Type | Description |
|---|---|
LagrangianSource
|
The source, with a one-cell emission pattern. |
Raises:
| Type | Description |
|---|---|
DomainError
|
If the location lies outside the footprint grid. |
Source code in src/plumax/coupled/_src/backends.py
footprints
¶
Member footprints F_e at every receptor level, level fastest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Instrument
|
The observing instrument; its overpass time is the receptor time of the backward runs. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'member point cell']
|
One row per (receptor, level) pair, units s m² kg⁻¹. |
Source code in src/plumax/coupled/_src/backends.py
column_response
¶
Column per unit emission at the instrument's receptors.
Algorithm
- Backward footprints from every receptor level, per member.
- Average over the members: F̄ = mean_e F_e.
- Concentration of a unit source: c = ρ_air F̄ q.
- Column and averaging kernel: r_i = A_i Σ_k h_k c_k(x_i).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Instrument
|
The observing instrument. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' obs'] | Float[Array, 'obs param']
|
The response r_inst, one column per emission pattern when the |
Float[Array, ' obs'] | Float[Array, 'obs param']
|
pattern has a parameter axis. |
Source code in src/plumax/coupled/_src/backends.py
TransportBackend
¶
Bases: Protocol
A transport tier as the coupled forward sees it.
r_inst = A_inst col_z(c(·; Q = 1)), the column per unit emission at each of the instrument's receptors.
Attributes:
| Name | Type | Description |
|---|---|---|
background |
float
|
Additive background column c_bg, in the units of the observation. |
column_response
¶
Column per unit emission at the instrument's receptors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Instrument
|
The observing instrument (receptors and kernel). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' obs'] | Float[Array, 'obs param']
|
The response r_inst: one value per receptor for a scalar emission |
Float[Array, ' obs'] | Float[Array, 'obs param']
|
rate, or one column per emission parameter. |
Source code in src/plumax/coupled/_src/backends.py
CoupledForward
dataclass
¶
CoupledForward(source: PlumeSource | TransportBackend, instruments: tuple[Instrument, ...], responses: tuple[Array, ...])
Multi-instrument coupled forward: a list of per-instrument observations.
Holds the shared source/met configuration and the instrument list, and
pre-computes each instrument's unit-Q :func:column_response (the static,
Q-independent part of the forward). Use :func:build_coupled_forward.
Attributes:
| Name | Type | Description |
|---|---|---|
source |
PlumeSource | TransportBackend
|
Shared transport backend ( |
instruments |
tuple[Instrument, ...]
|
The observing instruments (fusion list). |
responses |
tuple[Array, ...]
|
Per-instrument unit- |
predict
¶
predict(emission_rate: Array | float, biases: Sequence[Array | float] | None = None) -> list[jax.Array]
Predicted observation vector per instrument (list, native resolution).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
emission_rate
|
Array | float
|
Emission rate |
required |
biases
|
Sequence[Array | float] | None
|
Per-instrument additive biases, one per instrument. Defaults to zero for every instrument. |
None
|
Returns:
| Type | Description |
|---|---|
list[Array]
|
A list of |
Source code in src/plumax/coupled/forward.py
PlumeSource
dataclass
¶
PlumeSource(location: tuple[float, float, float], wind_speed: float, wind_direction: float, stability_class: str, column_z: Array, background: float = 0.0)
Static source + meteorology for the Tier I coupled forward.
The emission rate Q is deliberately not stored here — it is the scalar
the inversion solves for, so the forward is parameterised by everything
except Q (location, wind, stability, background) and is linear in it.
Attributes:
| Name | Type | Description |
|---|---|---|
location |
tuple[float, float, float]
|
|
wind_speed |
float
|
Wind speed [m/s], |
wind_direction |
float
|
Meteorological direction [deg from North] ( |
stability_class |
str
|
Pasquill class |
column_z |
Array
|
1-D vertical grid [m] for the column integral, shape
|
background |
float
|
Additive background column (same units as the AK output),
applied per receptor. Default |
FusionPosterior
dataclass
¶
FusionPosterior(emission_rate: Array, emission_std: Array, biases: Array, bias_std: Array, mean: Array, covariance: Array, instrument_names: tuple[str, ...], pixels_used: Array | None = None, prior_mean: Array | None = None, prior_covariance: Array | None = None)
Closed-form joint posterior over (Q, bias₁ … bias_M).
All numeric fields are kept as JAX arrays (the scalars as 0-d arrays) so the
whole posterior stays differentiable / jittable — the design advertises the
estimator for gradient-based workflows, and forcing float(...) here would
raise a concretization error under jax.grad / jax.jit. Use
float(post.emission_rate) at the call site when a Python scalar is wanted.
With design-matrix responses (n_param emission parameters, e.g. one per
cell of a Tier II source) the emission fields are (n_param,) vectors and
the state is [q₁ … q_P, bias₁ … bias_M].
Attributes:
| Name | Type | Description |
|---|---|---|
emission_rate |
Array
|
Posterior mean emission |
emission_std |
Array
|
Posterior standard deviation of |
biases |
Array
|
Posterior mean per-instrument biases, shape |
bias_std |
Array
|
Posterior standard deviations of the biases, shape
|
mean |
Array
|
Full posterior mean state |
covariance |
Array
|
Full posterior covariance |
instrument_names |
tuple[str, ...]
|
Names aligned with the bias entries. |
pixels_used |
Array | None
|
Pixels each instrument contributed at full weight
(unmasked, not inflated), shape |
prior_mean |
Array | None
|
Prior mean state θ_b the fusion used ( |
prior_covariance |
Array | None
|
Prior covariance |
For a scalar Q it also carries the Tier V per-event payload
(population.PerEventPosterior): Q's posterior marginal
N(Q, P₀₀) and its prior marginal N(θ_b,0, B₀₀) are exact.
samples
¶
n exact draws of Q from its posterior marginal N(Q, P₀₀).
Raises:
| Type | Description |
|---|---|
PayloadError
|
For design-matrix (vector) emissions. |
Source code in src/plumax/coupled/fusion.py
prior_logpdf
¶
log π(Q) of Q's prior marginal N(θ_b,0, B₀₀).
Raises:
| Type | Description |
|---|---|
PayloadError
|
For vector emissions, or without the prior. |
Source code in src/plumax/coupled/fusion.py
Instrument
dataclass
¶
Instrument(name: str, receptors: Array, averaging_kernel: Array | None = None, retrieval_variance: Array | float = 1.0, representation_variance: Array | float = 0.0, alignment_variance: Array | float = 0.0, observation_time: float = 0.0, flags: Array | None = None, mask: Array | None = None, variance_inflation: Array | None = None)
Fixed observation geometry + error budget for one satellite instrument.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Instrument identifier (e.g. |
receptors |
Array
|
Ground sample locations |
averaging_kernel |
Array | None
|
Per-receptor scalar AK |
retrieval_variance |
Array | float
|
Per-receptor L2 retrieval error |
representation_variance |
Array | float
|
Model-vs-footprint mismatch |
alignment_variance |
Array | float
|
Temporal-misalignment error |
observation_time |
float
|
Overpass time [s] (used by time-resolved transport
tiers; ignored by the steady-state Tier I forward). Default |
flags |
Array | None
|
Per-pixel quality bit flags, shape |
mask |
Array | None
|
Per-pixel observation mask, shape |
variance_inflation |
Array | None
|
Per-pixel factor |
observation_mask
¶
Per-pixel mask as a float array, 1 = used (all ones if unset).
observation_variance
¶
Total per-receptor observation variance R_retr + R_repr + R_align.
Each term broadcasts to (n_obs,). The design flags omitting the
representation / alignment terms as a source of overconfident posteriors,
so all three are summed here. A variance_inflation multiplies the
sum pixel by pixel.
Raises:
| Type | Description |
|---|---|
ValueError
|
if any resulting variance is non-positive (a zero/negative variance would blow up or invert the likelihood weight). |
Source code in src/plumax/coupled/instrument.py
CoupledPrior
dataclass
¶
CoupledPrior(mean: Mapping[str, Float[Array, '*shape']], covariance: Float[Array, ' param'] | Float[Array, 'param param'])
Gaussian prior N(μ_θ, Σ_θ) on the stacked control, by named block.
Attributes:
| Name | Type | Description |
|---|---|---|
mean |
Mapping[str, Float[Array, '*shape']]
|
Prior mean μ_θ of each block. Exactly one of |
covariance |
Float[Array, ' param'] | Float[Array, 'param param']
|
Σ_θ over the flattened control, in :attr: |
Raises:
| Type | Description |
|---|---|
ValueError
|
On an unknown or missing block, a mis-shaped block or a covariance that does not match the control. |
layout
property
¶
(block, length) pairs in the flattened order (sorted by name).
log_emission
property
¶
Whether the emission is lognormal (controlled through log Q).
from_std
classmethod
¶
from_std(mean: Mapping[str, Float[Array, '*shape'] | float], std: Mapping[str, Float[Array, '*shape'] | float]) -> CoupledPrior
A diagonal prior from per-block means and standard deviations.
Σ_θ = diag(σ²), each block's σ broadcast to the block's shape.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mean
|
Mapping[str, Float[Array, '*shape'] | float]
|
Prior mean μ_θ of each block. |
required |
std
|
Mapping[str, Float[Array, '*shape'] | float]
|
Prior standard deviation σ of each block, same keys. |
required |
Returns:
| Type | Description |
|---|---|
CoupledPrior
|
The prior. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the keys differ or a deviation is not positive. |
Source code in src/plumax/coupled/_src/inversion.py
flatten
¶
μ_θ as one vector, and the map from such a vector back to blocks.
Source code in src/plumax/coupled/_src/inversion.py
cholesky
¶
L with Σ_θ = L Lᵀ (√σ² on the diagonal for a diagonal prior).
Source code in src/plumax/coupled/_src/inversion.py
row_operator
¶
Σ_θ on a (1, D_θ) control row, the shape vardax applies it to.
Source code in src/plumax/coupled/_src/inversion.py
CoupledProblem
dataclass
¶
CoupledProblem(forward: CoupledForward, prior: CoupledPrior, observations: Mapping[str, Float[Array, '*shape']], radiance_ops: Mapping[str, RadianceOperator] = dict(), radiance_variance: Mapping[str, Float[Array, '*shape'] | float] = dict())
The coupled forward, its prior and every instrument's observations.
Column instruments are observed through their averaging kernel (the
CoupledForward responses); an instrument named in radiance_ops is
observed through that operator instead, applied to its column (bias
included). Masks and variance inflation come from each instrument
(coupled.quality.apply_quality).
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
CoupledForward
|
The coupled forward. |
prior |
CoupledPrior
|
Prior on the control. |
observations |
Mapping[str, Float[Array, '*shape']]
|
Observations by instrument name: |
radiance_ops |
Mapping[str, RadianceOperator]
|
JAX radiance operators by instrument name. |
radiance_variance |
Mapping[str, Float[Array, '*shape'] | float]
|
Radiance error variance by instrument name, a
scalar or |
Raises:
| Type | Description |
|---|---|
ValueError
|
On missing or extra observations, a met block without a
|
TypeError
|
For a |
responses
¶
Unit-emission response of each instrument at this control.
Source code in src/plumax/coupled/_src/inversion.py
predict
¶
h_i(θ) for every instrument, by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
control
|
Mapping[str, Array]
|
The control, by block (as :meth: |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Array]
|
Columns |
Source code in src/plumax/coupled/_src/inversion.py
predict_stacked
¶
Every instrument's prediction, flattened and stacked, shape (D_y,).
Source code in src/plumax/coupled/_src/inversion.py
stacked_observations
¶
Observations, masks and Σ_ε stacked like :meth:predict_stacked.
A radiance pixel's mask and variance inflation apply to all its channels. Masked values are zero (jejjohnson/vardax#147).
Source code in src/plumax/coupled/_src/inversion.py
CoupledResult
dataclass
¶
CoupledResult(map_estimate: dict[str, Array], std: dict[str, Array] | None, mean: Array, covariance: Array | None, emission_rate: Array, emission_std: Array, converged: bool, n_steps: int, cost: float, per_instrument_residuals: dict[str, Array], pixels_used: dict[str, int], prior: CoupledPrior | None = None)
MAP and Laplace posterior of the coupled inversion.
emission_rate and emission_std are the posterior mean and standard
deviation of Q (population.PerEventPosterior): for a lognormal Q the
lognormal moments of the Laplace marginal on log Q,
E[Q] = exp(μ + σ²/2) and sd[Q] = E[Q] √(exp(σ²) − 1).
Attributes:
| Name | Type | Description |
|---|---|---|
map_estimate |
dict[str, Array]
|
MAP θ* by block ( |
std |
dict[str, Array] | None
|
Laplace posterior standard deviation by block ( |
mean |
Array
|
MAP θ* flattened, in |
covariance |
Array | None
|
Laplace covariance Σ_θ|y, |
emission_rate |
Array
|
Posterior mean of Q. |
emission_std |
Array
|
Posterior standard deviation of Q (NaN without the posterior). |
converged |
bool
|
optimistix's success flag for the L-BFGS solve. |
n_steps |
int
|
Iterations taken. |
cost |
float
|
𝒥 at the MAP, in the whitened control. |
per_instrument_residuals |
dict[str, Array]
|
y_i − h_i(θ*) by instrument, NaN where masked. |
pixels_used |
dict[str, int]
|
Pixels each instrument contributed at full weight. |
prior |
CoupledPrior | None
|
The prior the solve used ( |
For a scalar Q it also carries the Tier V per-event payload
(population.PerEventPosterior): draws of Q from the Laplace marginal
of its block, N(Q, σ²) or log Q ~ N(log Q, σ²) for a lognormal Q, and the
log-density of that block's prior marginal, both exact for the Gaussian
laws on θ.
samples
¶
n draws of Q from the Laplace marginal of its block.
Raises:
| Type | Description |
|---|---|
PayloadError
|
Without the Laplace posterior, or for a vector Q. |
Source code in src/plumax/coupled/_src/inversion.py
prior_logpdf
¶
log π(Q) of the prior marginal of Q's block.
Raises:
| Type | Description |
|---|---|
PayloadError
|
Without the prior, or for a vector Q. |
Source code in src/plumax/coupled/_src/inversion.py
MaskedKPrior
dataclass
¶
MaskedKPrior(k_max: int = 10, prior_active: float = 0.3, emission_mean: float = 0.1, emission_std: float = 0.05, background_std: float = 1e-06)
The slot prior: how many slots, how likely each is on, and its Q_k.
a_k ~ Bernoulli(p_active), Q_k ~ LogNormal moment-matched to the real-scale mean
and standard deviation, as :func:plumax.gauss_plume.inference.multi_source_plume_model.
Attributes:
| Name | Type | Description |
|---|---|---|
k_max |
int
|
K_max ≥ 1 (the design's v1 cap is 10). |
prior_active |
float
|
p_active in (0, 1]; 1 keeps every slot on, with no discrete sites. |
emission_mean |
float
|
Real-scale prior mean of each Q_k [kg/s]. |
emission_std |
float
|
Real-scale prior standard deviation of each Q_k [kg/s]. |
background_std |
float
|
HalfNormal scale of the shared background column. |
Raises:
| Type | Description |
|---|---|
ValueError
|
On a parameter out of range. |
FlagPolicy
¶
Bases: StrEnum
What a rejected pixel becomes: dropped, or kept with an inflated variance.
QualityFlag
¶
Bases: IntFlag
Per-pixel quality bits, 0 = clear.
The bits of the planned radtrax retrieval flags (jejjohnson/radtrax#110, which supersedes plumax#97). Any integer bitmask works; these name the common ones.
QualityPolicy
dataclass
¶
QualityPolicy(policy: FlagPolicy = FlagPolicy.MASK, inflate_factor: float = 10.0, reject: int = QualityFlag.CLOUD | QualityFlag.NOT_CONVERGED)
Which flags reject a pixel, and how a rejected pixel enters the fusion.
Attributes:
| Name | Type | Description |
|---|---|---|
policy |
FlagPolicy
|
|
inflate_factor |
float
|
Variance factor κ ≥ 1 for |
reject |
int
|
Bitmask of the flags that reject a pixel. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
rejected
¶
Whether each pixel carries a rejecting flag, (flags & reject) ≠ 0.
StackedObservations
¶
Bases: Module
Every instrument's observations, mask and variance on one stacked axis.
The form vardax takes: y with masked pixels filled by zeros, the mask m and the diagonal of R (inflation included), instrument after instrument.
Attributes:
| Name | Type | Description |
|---|---|---|
values |
Float[Array, ' obs']
|
Observations y, zero where masked. |
mask |
Float[Array, ' obs']
|
Observation mask m, |
variance |
Float[Array, ' obs']
|
Diagonal of the observation-error covariance R. |
batch
¶
One-window vardax.Batch1D of shape (1, 1, D_y).
Raises:
| Type | Description |
|---|---|
ImportError
|
Without the |
Source code in src/plumax/coupled/_src/quality.py
obs_cov_op
¶
R as a diagonal operator on a (1, D_y) observation row.
A diagonal operator rather than a scaled identity, which vardax mis-solves (jejjohnson/vardax#146).
Source code in src/plumax/coupled/_src/quality.py
LayeredRadianceOperator
¶
Bases: Module
Map a column-mass enhancement to band radiance through radtrax's layered model.
The JAX counterpart of :class:RadianceObservationOperator, on
:class:radtrax.ForwardModel instead of the single slab::
column mass m [kg/m²] → ΔX = m / M_CH₄ [mol/m²]
→ state.add_column(gas, ΔX, profile) → y = F(state, surface, geometry)
and, when normalise (the default), y(ΔX) / y(0), so zero
enhancement gives ones as the slab operator's normalised radiance does.
Every step is JAX, so jax.grad of a misfit flows through the plume
model and the radiative transfer together, and predict is a
jax.vmap over receptors. The layers remove the slab's error of
broadening every line at one pressure; with one layer on a table node,
an identity instrument and no shift it equals the slab operator.
Band values are divided after band integration, y_b(ΔX) / y_b(0),
which is what a sensor's ratio is. The slab operator instead integrates
exp(-Δτ) over each band, so with an SRF the two differ by the
background spectrum's weighting within the band.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
ForwardModel
|
The layered forward model (opacity, solver, instrument). |
state |
AtmosphericState
|
The background atmosphere; it must carry |
surface |
Surface
|
The surface reflectance. |
geometry |
ViewingGeometry
|
Sun and sensor directions. |
profile |
Float[Array, ' L'] | None
|
The plume's relative weight per layer, |
gas |
str
|
The gas the plume adds. |
normalise |
bool
|
Divide by the background band values |
Examples:
>>> import jax.numpy as jnp
>>> from radtrax import ForwardModel
>>> from radtrax.atmosphere import ViewingGeometry, layers_from_levels
>>> from radtrax.instrument import BandInstrument
>>> from radtrax.solvers import BeerLambert
>>> from radtrax.spectroscopy import CrossSectionTable, TableOpacity
>>> from radtrax.surface import Lambertian
>>> nu = jnp.array([4300.0, 4300.1, 4300.2])
>>> table = CrossSectionTable(
... jnp.log(jnp.array([1e4, 1e5])),
... jnp.array([200.0, 300.0]),
... jnp.full((2, 2, 3), 1e-21),
... )
>>> model = ForwardModel(
... TableOpacity(nu, {"CH4": table}),
... BeerLambert(),
... BandInstrument(nu, jnp.full((1, 3), 1 / 3)),
... )
>>> atm = layers_from_levels(
... jnp.array([1e5, 5e4]),
... jnp.array([288.0, 255.0]),
... {"CH4": jnp.full(2, 1.9e-6)},
... )
>>> op = LayeredRadianceOperator(
... model, atm, Lambertian(0.3), ViewingGeometry(1.0, 1.0)
... )
>>> float(op.predict_single(0.0)[0])
1.0
n_channels
property
¶
Number of output channels per receptor (the instrument's bands).
delta_column
¶
Column mass [kg/m²] → column enhancement ΔX [mol/m²].
predict_single
¶
Band values for one receptor, shape (n_channels,).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column_mass_kg_m2
|
Array | float
|
Scalar column mass enhancement [kg/m²]. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
|
Source code in src/plumax/coupled/rtm.py
predict
¶
Per-receptor band values, shape (n_obs, n_channels).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column_enhancement
|
Array | ndarray
|
Per-receptor column mass enhancement [kg/m²],
shape |
required |
Source code in src/plumax/coupled/rtm.py
LinearisedRadianceOperator
dataclass
¶
Tangent-linear radiance operator — the linearised counterpart of the RTM.
The full :class:RadianceObservationOperator is exponential in the column
(L_norm = exp(-Δτ)), so it has no closed-form inversion. Linearising the
normalised Beer–Lambert forward about the background (ΔVMR = 0, where
L_norm = 1) gives the affine response
L_norm(ΔVMR) ≈ 1 + J₀ · ΔVMR, J₀ = d(L_norm)/d(ΔVMR)|₀,
and since ΔVMR = slope · column_mass is itself linear (the constant slope
of :func:column_mass_to_delta_vmr) and the Tier I column is linear in the
emission rate Q, the whole radiance forward becomes linear in Q.
That makes this the radiance analogue of the scalar averaging kernel: a
per-channel linear gain J₀ · slope on the column mass, usable as a
drop-in linear observation response (and, with one channel, in the
closed-form fusion path the scalar AK uses).
This is the well-known linearised / optimal-estimation radiance model (first-order Beer–Lambert), complementary to the exact nonlinear operator; it is accurate for small enhancements and exact in the optically-thin limit.
Build with :func:linearise from a :class:RadianceObservationOperator so
the two share the LUT, geometry and SRF configuration.
Attributes:
| Name | Type | Description |
|---|---|---|
baseline |
Array
|
Background normalised radiance |
gain |
Array
|
Per-channel linear response to column mass |
predict_single
¶
Linearised normalised radiance baseline + gain · mass for one receptor.
predict
¶
Per-receptor linearised radiance, shape (n_obs, n_channels).
Fully differentiable / vectorised JAX (unlike the nonlinear operator's
NumPy LUT path): baseline[None, :] + mass[:, None] · gain[None, :].
Source code in src/plumax/coupled/rtm.py
RadianceObservationOperator
dataclass
¶
RadianceObservationOperator(ds: Dataset, nu_obs: ndarray, T_K: float, p_atm: float, vmr_background: float, path_length_cm: float, amf: float, srf: SpectralResponseFunction | None = None, var: str = 'absorption_cross_section')
Map a per-receptor column-enhancement field to band radiances via the RTM.
The RTM analogue of the scalar averaging kernel: instead of
y = A · column, this composes
column mass [kg/m²] → ΔVMR → L_norm(ν) = exp(-Δτ) → band radiance
using :func:column_mass_to_delta_vmr and the normalised Beer–Lambert
forward :func:radtrax.slab.forward_nonlinear_normalized. With srf
supplied, the high-resolution normalised radiance exp(-Δτ(ν)) is
band-integrated by the instrument :class:~radtrax.instrument.SpectralResponseFunction;
otherwise the full nu_obs spectrum is returned per receptor.
The forward is NumPy-side (the LUT interpolation is not JAX-traceable),
matching :class:plumax.adapters.RadtranObservationOperator; the analytic
d(L_norm)/d(ΔVMR) Jacobian is exposed through :meth:linearize.
Attributes:
| Name | Type | Description |
|---|---|---|
ds |
Dataset
|
Absorption-cross-section LUT dataset (HAPI schema; see
:func: |
nu_obs |
ndarray
|
Observation wavenumbers [cm⁻¹], shape |
T_K |
float
|
Layer temperature [K]. |
p_atm |
float
|
Layer pressure [atm]. |
vmr_background |
float
|
Background gas volume mixing ratio. |
path_length_cm |
float
|
Optical / absorbing-path length [cm]. |
amf |
float
|
Air-mass factor. |
srf |
SpectralResponseFunction | None
|
Optional band-integration operator. When |
var |
str
|
LUT cross-section variable name. |
n_channels
property
¶
Number of output channels per receptor (bands if SRF, else n_nu).
delta_vmr
¶
Column mass [kg/m²] → ΔVMR via :func:column_mass_to_delta_vmr.
Source code in src/plumax/coupled/rtm.py
predict_single
¶
Band-integrated normalised radiance for one receptor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column_mass_kg_m2
|
float
|
Scalar column mass enhancement [kg/m²]. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Normalised radiance, shape |
ndarray
|
integrated when an SRF is configured). |
Source code in src/plumax/coupled/rtm.py
predict
¶
Per-receptor band-integrated normalised radiance (vectorised).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column_enhancement
|
Array | ndarray
|
Per-receptor column mass enhancement [kg/m²],
shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Per-receptor normalised radiance, shape |
Source code in src/plumax/coupled/rtm.py
linearize_single
¶
Band-integrated d(L_norm)/d(ΔVMR) at one receptor.
Returns the radtrax slab analytic Jacobian, band-integrated through the SRF
when configured. This is the tangent-linear w.r.t. ΔVMR; chain with
d(ΔVMR)/d(column mass) (the constant slope of
:func:column_mass_to_delta_vmr) for the column-space Jacobian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column_mass_kg_m2
|
float
|
Scalar column mass enhancement [kg/m²]. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Jacobian, shape |
Source code in src/plumax/coupled/rtm.py
build_coupled_forward
¶
build_coupled_forward(source: PlumeSource | TransportBackend, instruments: Sequence[Instrument]) -> CoupledForward
Assemble a :class:CoupledForward, precomputing unit-Q responses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
PlumeSource | TransportBackend
|
Shared transport backend: the Tier I |
required |
instruments
|
Sequence[Instrument]
|
One or more observing instruments to fuse. |
required |
Returns:
| Type | Description |
|---|---|
CoupledForward
|
The configured :class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Source code in src/plumax/coupled/forward.py
column_response
¶
Per-receptor column enhancement at unit emission for one instrument.
Evaluates the Gaussian plume at unit Q on each receptor column, then
integrates over source.column_z (trapezoidal) and applies the instrument
averaging kernel with the shared
:class:~plumax.gauss_plume.observation.ColumnObservation (whose module
docstring states the units). The full observation at emission Q is then
Q · column_response + bias (plus the background; see
:func:predict_observation). Linearity in Q is what makes the coupled
inversion closed-form.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
PlumeSource
|
The static source + met configuration. |
required |
instrument
|
Instrument
|
The observing instrument (receptors + AK). |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Unit- |
Source code in src/plumax/coupled/forward.py
predict_observation
¶
predict_observation(source: PlumeSource, instrument: Instrument, emission_rate: Array | float, bias: Array | float = 0.0) -> jax.Array
Predict one instrument's observation vector y_inst.
y = Q · column_response + background + bias — the affine forward the
coupled cost compares against the observed enhancement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
PlumeSource
|
Static source + met configuration. |
required |
instrument
|
Instrument
|
The observing instrument. |
required |
emission_rate
|
Array | float
|
Emission rate |
required |
bias
|
Array | float
|
Per-instrument additive offset (scalar). Default |
0.0
|
Returns:
| Type | Description |
|---|---|
Array
|
Predicted observation, shape |
Source code in src/plumax/coupled/forward.py
default_prior
¶
default_prior(*, n_instruments: int, emission_prior_mean: float, emission_prior_std: float, bias_prior_std: float) -> tuple[jax.Array, jax.Array]
Build a diagonal prior (θ_b, B) for (Q, bias₁ … bias_M).
A convenience for the common case: a Gaussian prior on Q and zero-mean
Gaussian biases (the design's bias_inst ~ N(0, σ²_inst)).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_instruments
|
int
|
Number of instruments (bias slots). |
required |
emission_prior_mean
|
float
|
Prior mean for |
required |
emission_prior_std
|
float
|
Prior std for |
required |
bias_prior_std
|
float
|
Prior std for each per-instrument bias ( |
required |
Returns:
| Type | Description |
|---|---|
tuple[Array, Array]
|
|
Source code in src/plumax/coupled/fusion.py
fuse_observations
¶
fuse_observations(forward: CoupledForward, observations: Sequence[Array], *, prior_mean: Array, prior_covariance: Array, quality: QualityPolicy | None = None) -> FusionPosterior
Closed-form joint posterior over (Q, bias_inst) from fused observations.
Solves the BLUE update for the affine forward y = G θ + c_bg with a
Gaussian prior θ ~ N(θ_b, B) and diagonal observation covariance R
(the per-instrument R_retr + R_repr + R_align)::
P*⁻¹ = B⁻¹ + Gᵀ R⁻¹ G
θ* = θ_b + P* Gᵀ R⁻¹ (y − G θ_b − c_bg)
solved as a vardax.OptimalInterpolation analysis in its state-space
(information) form, which factorises only the small
(n_param + n_inst) × (n_param + n_inst) posterior precision.
Each instrument's mask zeroes its pixels' weight in R⁻¹ and its
variance_inflation scales their variance (see
:mod:plumax.coupled.quality); a masked observation may hold anything,
NaN included.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forward
|
CoupledForward
|
The assembled multi-instrument coupled forward. |
required |
observations
|
Sequence[Array]
|
Observed enhancement vector per instrument, aligned with
|
required |
prior_mean
|
Array
|
Prior mean state |
required |
prior_covariance
|
Array
|
Prior covariance |
required |
quality
|
QualityPolicy | None
|
Optional quality policy, applied to every instrument that
carries |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
FusionPosterior
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
on a per-instrument observation-count mismatch or a mis-shaped prior. |
ImportError
|
Without the |
Source code in src/plumax/coupled/fusion.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 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 | |
solve_coupled
¶
solve_coupled(problem: CoupledProblem, *, method: Literal['lbfgs', 'gauss_newton'] = 'lbfgs', laplace: bool = True, rtol: float = 1e-08, atol: float = 1e-10, max_steps: int = 200) -> CoupledResult
MAP and Laplace posterior of the coupled problem, on vardax.
θ = argmin 𝒥(θ), Σ_θ|y = (Σ_θ⁻¹ + J_hᵀ Σ_ε⁻¹ J_h)⁻¹ at θ.
Algorithm
- Flatten the prior to μ_θ, factor Σ_θ = L Lᵀ.
- Stack observations, masks and Σ_ε over the instruments.
vardax.ThreeDVarwith the stacked forward as aCallableObsand the whitened control θ = μ_θ + L χ; minimise with L-BFGS.vardax.LaplaceCovarianceat θ*, materialised.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
CoupledProblem
|
The coupled problem. |
required |
method
|
Literal['lbfgs', 'gauss_newton']
|
|
'lbfgs'
|
laplace
|
bool
|
Also compute the Laplace posterior. |
True
|
rtol
|
float
|
L-BFGS relative tolerance. |
1e-08
|
atol
|
float
|
L-BFGS absolute tolerance. |
1e-10
|
max_steps
|
int
|
Iteration cap. |
200
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
CoupledResult
|
class: |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
For |
ImportError
|
Without the |
Source code in src/plumax/coupled/_src/inversion.py
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 | |
build_masked_k_problem
¶
build_masked_k_problem(instruments: Sequence[Instrument], *, transport: PlumeSource | TransportBackend, prior: MaskedKPrior, observations: Mapping[str, Array], active: Bool[Array, ' source'], candidate_locations: Float[Array, 'source 2'] | None = None) -> CoupledProblem
The coupled problem of one active set, for the MAP and Laplace path.
y_i = Σ_{k active} Q_k r_i(x_k, y_k) + c_bg + ε_i over log Q_k and c_bg, under the
same lognormal prior as the slots. The background is the Gaussian N(0, σ²_bg) here,
against the sampler's HalfNormal(σ_bg): the block prior is Gaussian, and the two
have the same curvature at zero. Hand it to :func:~plumax.coupled.solve_coupled,
which solves on vardax. This is the operational path; the active set itself comes
from :func:infer_masked_k_sources or from the caller.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instruments
|
Sequence[Instrument]
|
The observing instruments. |
required |
transport
|
PlumeSource | TransportBackend
|
The transport backend. |
required |
prior
|
MaskedKPrior
|
The slot prior. |
required |
observations
|
Mapping[str, Array]
|
Observations by instrument. |
required |
active
|
Bool[Array, ' source']
|
a_k, with at least one slot on. |
required |
candidate_locations
|
Float[Array, 'source 2'] | None
|
Fixed |
None
|
Returns:
| Type | Description |
|---|---|
CoupledProblem
|
The problem, with a |
CoupledProblem
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
When |
Source code in src/plumax/coupled/_src/k_sources.py
infer_masked_k_sources
¶
infer_masked_k_sources(observations: Mapping[str, Array], *, instruments: Sequence[Instrument], transport: PlumeSource | TransportBackend, prior: MaskedKPrior, candidate_locations: Float[Array, 'source 2'] | None = None, source_bounds: tuple[tuple[float, float], tuple[float, float]] | None = None, num_warmup: int = 500, num_samples: int = 1000, seed: int = 0) -> MultiSourcePosterior
Posterior over the active count and the per-slot Q_k, from every instrument.
Tier I's runner on :func:masked_k_coupled_model: NUTS for the continuous sites,
a Gibbs step for the a_k.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
observations
|
Mapping[str, Array]
|
Column observations by instrument. |
required |
instruments
|
Sequence[Instrument]
|
The observing instruments. |
required |
transport
|
PlumeSource | TransportBackend
|
The transport backend. |
required |
prior
|
MaskedKPrior
|
The slot prior. |
required |
candidate_locations
|
Float[Array, 'source 2'] | None
|
Fixed |
None
|
source_bounds
|
tuple[tuple[float, float], tuple[float, float]] | None
|
Box of free slots, instead of candidates. |
None
|
num_warmup
|
int
|
NUTS warm-up steps. |
500
|
num_samples
|
int
|
Posterior draws. |
1000
|
seed
|
int
|
PRNG seed. |
0
|
Returns:
| Type | Description |
|---|---|
MultiSourcePosterior
|
The posterior: |
MultiSourcePosterior
|
the slot's summary, which |
Raises:
| Type | Description |
|---|---|
ValueError
|
On missing observations or an invalid slot layout. |
Source code in src/plumax/coupled/_src/k_sources.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | |
masked_k_coupled_model
¶
masked_k_coupled_model(observations: Mapping[str, Array] | None, *, instruments: Sequence[Instrument], transport: PlumeSource | TransportBackend, k_max: int, candidate_locations: Float[Array, 'source 2'] | None = None, source_bounds: tuple[tuple[float, float], tuple[float, float]] | None = None, prior_active: float = 0.3, emission_mean: float = 0.1, emission_std: float = 0.05, background_std: float = 1e-06, fixed_responses: Sequence[Array] | None = None) -> None
NumPyro masked-K model over every instrument's columns.
y_i ~ Normal(Σ_k a_k Q_k r_i(x_k, y_k) + c_bg, Σ_ε,i), with Σ_ε,i and the mask of
each :class:~plumax.coupled.Instrument, under Tier I's slot prior. With
source_bounds the slots move, so a PlumeSource is rerun at each draw.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
observations
|
Mapping[str, Array] | None
|
Column observations by instrument, or |
required |
instruments
|
Sequence[Instrument]
|
The observing instruments. |
required |
transport
|
PlumeSource | TransportBackend
|
The transport backend. |
required |
k_max
|
int
|
K_max. |
required |
candidate_locations
|
Float[Array, 'source 2'] | None
|
Fixed |
None
|
source_bounds
|
tuple[tuple[float, float], tuple[float, float]] | None
|
Box of free, x-ordered slots; needs a |
None
|
prior_active
|
float
|
p_active. |
0.3
|
emission_mean
|
float
|
Real-scale prior mean of each Q_k [kg/s]. |
0.1
|
emission_std
|
float
|
Real-scale prior standard deviation of each Q_k [kg/s]. |
0.05
|
background_std
|
float
|
HalfNormal scale of the background column. |
1e-06
|
fixed_responses
|
Sequence[Array] | None
|
:func: |
None
|
Source code in src/plumax/coupled/_src/k_sources.py
saturation_rate
¶
Fraction of overpasses whose most probable active count is K_max.
K̂ = argmax_k P(K = k | y), rate = #{K̂ = K_max} / N. Above 0.05 the cap is binding and the RJMCMC model is due (see the module docstring).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
posteriors
|
Sequence[MultiSourcePosterior]
|
One masked-K posterior per overpass. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The saturated fraction. |
Raises:
| Type | Description |
|---|---|
ValueError
|
With no posteriors. |
Examples:
One overpass at the cap, one below it:
>>> import numpy as np
>>> def posterior(active):
... shape = np.shape(active)
... return MultiSourcePosterior(
... emission_rate=np.ones(shape),
... x=np.zeros(shape),
... y=np.zeros(shape),
... active=np.asarray(active),
... background=np.zeros(shape[0]),
... )
>>> full = posterior([[True, True], [True, True]])
>>> partial = posterior([[True, False], [True, False]])
>>> saturation_rate([full, partial])
0.5
Source code in src/plumax/coupled/_src/k_sources.py
slot_responses
¶
slot_responses(transport: PlumeSource | TransportBackend, instruments: Sequence[Instrument], candidate_locations: Float[Array, 'source 2'] | None = None) -> list[Array]
Unit-emission column of every slot, as one design matrix per instrument.
R_i[n, k] = r_i(x_k, y_k) at receptor n. The Tier I plume is rerun at each
candidate. Any other backend has its emission parameters as the slots already:
its column_response must return the (obs, source) matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
PlumeSource | TransportBackend
|
The transport backend. |
required |
instruments
|
Sequence[Instrument]
|
The observing instruments. |
required |
candidate_locations
|
Float[Array, 'source 2'] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
list[Array]
|
One |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a |
Source code in src/plumax/coupled/_src/k_sources.py
apply_quality
¶
apply_quality(instrument: Instrument, flags: Int[Array, ' obs'] | None = None, policy: QualityPolicy | None = None) -> Instrument
The instrument with its flags and the policy's mask or inflation set.
m_i = 0 for a rejected pixel under MASK; R_ii ← κ R_ii under
INFLATE. The geometry, kernel and error budget are unchanged, so the
coupled forward's responses stay valid. The result depends only on the
flags and the policy, so applying it twice is the same as once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
Instrument
|
The instrument to screen. |
required |
flags
|
Int[Array, ' obs'] | None
|
Per-pixel bit flags. Default: the instrument's own |
None
|
policy
|
QualityPolicy | None
|
The quality policy. Default |
None
|
Returns:
| Type | Description |
|---|---|
Instrument
|
A copy of |
Instrument
|
|
Examples:
Mask the cloudy pixel of three:
>>> import jax.numpy as jnp
>>> from plumax.coupled import Instrument
>>> instrument = Instrument("A", jnp.zeros((3, 2)))
>>> flags = jnp.array([0, QualityFlag.CLOUD, QualityFlag.AOD])
>>> apply_quality(instrument, flags).observation_mask().tolist()
[1.0, 0.0, 1.0]
Inflate instead, by 100:
>>> inflate = QualityPolicy(FlagPolicy.INFLATE, inflate_factor=100.0)
>>> apply_quality(instrument, flags, inflate).observation_variance().tolist()
[1.0, 100.0, 1.0]
Source code in src/plumax/coupled/_src/quality.py
contribution_report
¶
Pixels each instrument contributes at full weight, by name.
Masked pixels and inflated pixels both count as not contributing in full, so the report is the effective per-instrument observation count the design asks to keep auditable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instruments
|
Sequence[Instrument]
|
The instruments of a fusion. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If two instruments share a name. |
Examples:
>>> import jax.numpy as jnp
>>> from plumax.coupled import Instrument
>>> clear = Instrument("A", jnp.zeros((2, 2)))
>>> cloudy = apply_quality(
... Instrument("B", jnp.zeros((2, 2))), jnp.full(2, QualityFlag.CLOUD)
... )
>>> contribution_report([clear, cloudy])
{'A': 2, 'B': 0}
Source code in src/plumax/coupled/_src/quality.py
stack_observations
¶
stack_observations(instruments: Sequence[Instrument], observations: Sequence[Float[Array, ' _']]) -> StackedObservations
Stack the instruments' observations, masks and variances for vardax.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instruments
|
Sequence[Instrument]
|
The instruments, with any quality policy already applied. |
required |
observations
|
Sequence[Float[Array, ' _']]
|
One observation vector per instrument, aligned with
|
required |
Returns:
| Type | Description |
|---|---|
StackedObservations
|
The stacked observations, masked entries set to zero. |
Raises:
| Type | Description |
|---|---|
ValueError
|
On a count or shape mismatch. |
Examples:
A NaN under the mask becomes a zero:
>>> import jax.numpy as jnp
>>> from plumax.coupled import Instrument
>>> instrument = Instrument("A", jnp.zeros((2, 2)), mask=jnp.array([1.0, 0.0]))
>>> stacked = stack_observations([instrument], [jnp.array([3.0, jnp.nan])])
>>> stacked.values.tolist(), stacked.mask.tolist()
([3.0, 0.0], [1.0, 0.0])
Source code in src/plumax/coupled/_src/quality.py
column_mass_to_delta_vmr
¶
column_mass_to_delta_vmr(column_mass_kg_m2: Array | float, *, p_atm: float, T_K: float, path_length_cm: float) -> jax.Array
Convert a plume column mass enhancement [kg/m²] to a gas ΔVMR.
The Tier I plume column integral gives a column mass enhancement
m [kg/m²] of methane sitting above the pixel. The radtrax slab Beer–Lambert
forward instead parameterises absorption by the enhancement in volume
mixing ratio ΔVMR over the absorbing path. The two are related by the
column number densities of the gas and of dry air:
-
Gas column number density [molecules/cm²]::
N_gas = m · (N_A / M_CH₄) / CM2_PER_M2
(N_A = Avogadro, M_CH₄ = methane molar mass; dividing by
CM2_PER_M2 = 1e4 converts the per-m² areal density to per-cm².)
-
Air column number density [molecules/cm²]::
N_air = n(p, T) · L
where n(p, T) is the ideal-gas number density [molecules/cm³]
(:func:radtrax.slab.number_density_cm3) and L is the
vertical path length [cm] over which the enhancement is distributed.
-
Volume mixing ratio enhancement (dimensionless)::
ΔVMR = N_gas / N_air
This is the standard "slant/vertical column density divided by the air
column" relation used in trace-gas retrievals (cf. the partial-column-to-VMR
conversion in e.g. TROPOMI / matched-filter retrievals); keeping L and
(p, T) explicit makes the per-instrument geometry auditable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
column_mass_kg_m2
|
Array | float
|
Column mass enhancement [kg/m²] (scalar or array; the
Tier I plume column response is linear in the emission rate |
required |
p_atm
|
float
|
Layer pressure [atm], |
required |
T_K
|
float
|
Layer temperature [K], |
required |
path_length_cm
|
float
|
Vertical absorbing-path length [cm], |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The dimensionless |
Array
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Source code in src/plumax/coupled/rtm.py
linearise
¶
linearise(operator: RadianceObservationOperator | LayeredRadianceOperator) -> LinearisedRadianceOperator
Build the :class:LinearisedRadianceOperator tangent to operator at 0.
Evaluates the background radiance L_norm(0) and the analytic Jacobian
J₀ = d(L_norm)/d(ΔVMR)|₀ (both band-integrated through the operator's
SRF), and folds in the constant d(ΔVMR)/d(mass) slope of
:func:column_mass_to_delta_vmr to give the per-channel gain in column-mass
units. The result is exact to first order in the enhancement. For a
:class:LayeredRadianceOperator the gain is jax.jacfwd of its
predict_single at zero, already in column-mass units.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operator
|
RadianceObservationOperator | LayeredRadianceOperator
|
The configured nonlinear RTM observation operator. |
required |
Returns:
| Type | Description |
|---|---|
LinearisedRadianceOperator
|
The matching :class: |
Source code in src/plumax/coupled/rtm.py
radiance_response
¶
radiance_response(source: PlumeSource, instrument: Instrument, operator: RadianceObservationOperator | LayeredRadianceOperator, emission_rate: Array | float) -> np.ndarray | jax.Array
Per-receptor band radiances for one instrument under the RTM operator.
The radiance-forward parallel to :func:plumax.coupled.forward.predict_observation:
it evaluates the Tier I unit-Q column response
(:func:plumax.coupled.forward.column_response), scales by the emission rate
to get the per-receptor column mass enhancement
Q · column_response [kg/m²], then maps each receptor through the RTM
observation operator to a band-integrated normalised radiance.
Unlike the scalar-AK forward this is not linear in Q (Beer–Lambert
is exponential in the column), so it has no closed-form fusion path; it is
intended for radiance simulation and as the forward of a gradient-based /
linearised L1 inversion (future build-order step), leaving the existing
closed-form :func:plumax.coupled.fuse_observations AK path intact.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
PlumeSource
|
Static source + met configuration. |
required |
instrument
|
Instrument
|
Observing instrument (receptors + AK). The RTM operator is
the physical replacement for the scalar AK, so for a pure-RTM forward
leave the instrument's |
required |
operator
|
RadianceObservationOperator | LayeredRadianceOperator
|
The configured :class: |
required |
emission_rate
|
Array | float
|
Emission rate |
required |
Returns:
| Type | Description |
|---|---|
ndarray | Array
|
Per-receptor normalised radiance, shape |
Source code in src/plumax/coupled/rtm.py
radiance_response_linear
¶
radiance_response_linear(source: PlumeSource, instrument: Instrument, operator: LinearisedRadianceOperator, emission_rate: Array | float) -> jax.Array
Per-receptor linearised radiances for one instrument (differentiable).
The tangent-linear parallel of :func:radiance_response: it scales the
Tier I unit-Q column response by Q and maps each receptor through the
:class:LinearisedRadianceOperator. Unlike :func:radiance_response this is
linear in Q and stays in pure JAX end-to-end, so it is
jax.grad / jax.jit-friendly and (single-channel) feeds the
closed-form :func:plumax.coupled.fuse_observations path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
PlumeSource
|
Static source + met configuration. |
required |
instrument
|
Instrument
|
Observing instrument (receptors + AK). |
required |
operator
|
LinearisedRadianceOperator
|
The linearised radiance operator (from :func: |
required |
emission_rate
|
Array | float
|
Emission rate |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Per-receptor linearised normalised radiance, shape |