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:plumax.radtranBeer–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. The scalar-AK path above is unchanged.
Later build-order steps (Tier II/III transport + RTM, the Q(t) stochastic
process, trans-dimensional source count, 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.
CoupledForward
dataclass
¶
CoupledForward(source: PlumeSource, 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
|
Shared source + met configuration. |
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 |
wind_uv
¶
Wind (u, v) [m/s] from speed + meteorological direction.
Source code in src/plumax/coupled/forward.py
FusionPosterior
dataclass
¶
FusionPosterior(emission_rate: Array, emission_std: Array, biases: Array, bias_std: Array, mean: Array, covariance: Array, instrument_names: tuple[str, ...])
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.
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. |
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)
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 |
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.
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
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:plumax.radtran.forward_nonlinear_normalized. With srf
supplied, the high-resolution normalised radiance exp(-Δτ(ν)) is
band-integrated by the instrument :class:~plumax.radtran.srf.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 radtran 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
¶
Assemble a :class:CoupledForward, precomputing unit-Q responses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
PlumeSource
|
Shared source + met configuration. |
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, integrates
over source.column_z (trapezoidal), and applies the instrument averaging
kernel. 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) -> 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)::
θ* = θ_b + B Gᵀ (G B Gᵀ + R)⁻¹ (y − G θ_b − c_bg)
P* = B − B Gᵀ (G B Gᵀ + R)⁻¹ G B
solved in the (n_total_obs × n_total_obs) innovation form via Cholesky.
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 |
Returns:
| Name | Type | Description |
|---|---|---|
The |
FusionPosterior
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
on a per-instrument observation-count mismatch or a mis-shaped prior. |
Source code in src/plumax/coupled/fusion.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
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 radtran 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:plumax.radtran.config.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
¶
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.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operator
|
RadianceObservationOperator
|
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, emission_rate: Array | float) -> np.ndarray
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
|
The configured :class: |
required |
emission_rate
|
Array | float
|
Emission rate |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
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 |