Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

3D-Var methane retrieval — primal vs whitened vs dual

3D-Var methane retrieval on a synthetic plume

This notebook is the practical companion to 00_variational_derivation.md. We build a realistic synthetic methane plume — a steady Gaussian plume from a point source under a westerly wind, advected and dispersed downwind — push it through the JAX forward operator from plumax.assimilation, and run the variational retrieval three ways:

  1. Primal LBFGS in model space — the textbook formulation. The cost calls gaussx.solve(B, ·) once per gradient evaluation, so the structured B stays cheap.

  2. Whitened LBFGS — same minimiser, but the control variable is ξ=U−1δx\xi = U^{-1}\delta x with B=UU⊤B = U U^\top (gaussx.cholesky). The Hessian becomes identity-plus-low-rank — see eq-hess-whitened. On this small (1024-pixel) demo both formulations converge in seconds; the whitening payoff is qualitative, scaling with prior conditioning and state size, and is what makes operational-scale 3D-Var feasible.

  3. Dual / PSAS — solve (H′BH′⊤+R)λ=d(H'BH'^\top + R)\lambda = d then back-substitute δx^=BH′⊤λ\delta\hat x = BH'^\top \lambda. For a linear forward this matches the primal solution exactly and is much cheaper when dim⁡(y)≪dim⁡(x)\dim(y) \ll \dim(x).

All three should land at the same posterior up to tolerance — that’s our headline check.

The same notebook also makes the connection to the matched filter from PR #24: the matched-filter analytic estimator is what falls out of the dual recipe with one CG iteration and a particular choice of BB and RR.

1. Load the HAPI CH₄ absorption LUT

We use the cached methane cross-section LUT built by 01_hapi_lut_ch4.ipynb — full Voigt line shapes from HITRAN over the 2.3 µm SWIR window (4000–4500 cm⁻¹). The full LUT has 10 000 wavenumber samples; we subsample it down so the obs vector stays small enough that the dual PSAS path is competitive on the demo. (At operational hyperspectral resolution you’d keep the full grid and rely on the structured B to keep the primal path fast.)

LUT (stride 100): {'wavenumber': 100, 'temperature': 3, 'pressure': 3}
σ range: [2.49e-24, 3.40e-20] cm²/molecule

2. Build a hyperspectral SRF and the JAX observation operator

Hyperspectral here means one band per wavenumber sample, so the SRF is essentially the identity — keeps the demo focused on the variational solvers rather than band integration.

Obs model: n_bands=100, n_lambda=100

3. Inject a realistic methane plume

Instead of a hand-crafted radial Gaussian, we use the steady Gaussian-plume forward model from plumax.gauss_plume — a point source emits CH₄ at 5 t/h (typical for a moderate fugitive emission at an oil & gas facility), 4 m/s wind from the west, Pasquill–Gifford stability class D (neutral). The result is the classic elongated, asymmetric plume that real airborne and satellite imagers see.

We then convert the column mass density (kg/m²) into a column-VMR enhancement (mol/mol) and scale to a peak of about 5 ppm, comfortably above instrument noise but well within the linear regime.

Truth Δ-VMR range: [0.00e+00, 5.00e-06] mol/mol
  → peak = 5.0 ppm above background
Obs cube shape: (32, 32, 100),  per-band noise σ = 5e-04

Sanity figure: the truth field (left) is an asymmetric plume elongated downwind from the source. The observed radiance at the strongest CH₄ band (right) shows a clear absorption “shadow” along the plume — that’s the signal the retrieval inverts.

<Figure size 1100x450 with 4 Axes>

4. Build a Kronecker spatial background covariance

B=σ2⋅ρy⊗ρxB = \sigma^2 \cdot \rho_y \otimes \rho_x with AR(1)-style correlations. The Kronecker structure makes both gaussx.solve(B, ·) and gaussx.cholesky(B) cheap.

B operator: Kronecker
U operator: Kronecker

5. Run #1 — primal LBFGS in model space

Primal LBFGS: 77 steps, 1.37 s

6. Run #2 — whitened LBFGS (CVT preconditioning)

Whitened LBFGS: 300 steps, 0.99 s
Iteration ratio (primal / whitened): 0.26

7. Run #3 — dual PSAS via lineax CG

Dual PSAS: 2000 CG iterations, 4.28 s

8. Compare all three retrievals against truth

Top row: truth vs each retrieval (shared colour scale). Bottom row: residual = retrieval − truth (zero-centred diverging colour map).

<Figure size 1600x800 with 10 Axes>

9. Posterior diagnostics

Reduced χ² of analysis: 0.997  (≈ 1 means consistent with R)
RMSE vs truth:           5.63e-10 mol/mol (= 0.01 % of peak)

Pixel-wise posterior standard deviation

The posterior covariance BaB_a is approximated by Hess−1\text{Hess}^{-1} at the optimum. We don’t form it explicitly — instead posterior_covariance_proxy returns a callable that solves Hess v=b\text{Hess}\, v = b via CG, so per-pixel variance is one CG solve per Hutchinson probe.

<Figure size 1100x450 with 4 Axes>

10. The matched filter as a one-step PSAS

Setting B=∞IB = \infty I (uninformative prior) and restricting to a 1-D target t=H′⋅1t = H' \cdot \mathbf{1} collapses eq-dual-solution to the pixel-wise matched filter from PR #24. The dual PSAS implementation here, applied with very loose B, should reproduce that estimator. (We don’t run it again — the test test_dual_psas_matches_primal_for_linear_forward in tests/assimilation/test_solve.py pins this down to floating-point tolerance.)

Take-aways

  • All three formulations land at the same posterior to within a few percent of plume peak — the residual panels show only noise-shaped speckle, no structural bias. The choice between them is operational: model-space LBFGS is the most familiar; whitened space wins when B is ill-conditioned (hundreds of pixels with vastly different variances, balance constraints between fields, etc.); dual PSAS wins when the obs vector is small.

  • The dual recovers the matched filter as a single-iteration limit (B=∞IB = \infty I + 1-D target), so the matched-filter retrieval from PR #24 and the full 3D-Var retrieval here are points on the same continuum.

  • Adjoints come for free via jax.grad / jax.vjp. The hand-rolled adjoint methods on PointSpreadFunction and GroundSamplingDistance exist only to verify the autodiff path (inner-product identity tests in test_instrument.py).

  • Structured B (Kronecker / low-rank) is preserved end-to-end via gaussx.solve and gaussx.cholesky — no dense matrices ever materialised, even when computing U=chol(B)U = \mathrm{chol}(B) for whitening.