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.

Plume vetting: a spectral test for matched-filter candidates

Open In Colab

The matched filter scores each pixel by one projection of its spectrum, so anything that removes light under methane’s 2.3 μm band scores: carbonate soils, salt pans, shadows, striping. Plume vetting (Xiang et al. 2024; Růžička et al. 2026) goes back to the radiances. It pairs a candidate’s strongest pixels with pixels outside it that have the same surface, divides them, and fits methane’s transmittance times a smooth continuum to the ratio; the misfit DnormD_{\text{norm}} accepts or rejects the candidate. This notebook builds one EMIT-like scene in which the matched filter scores a carbonate patch above a real plume, and vets both.

The work comes from two packages. plumax.vetting works on plume candidates: their seeds and background pool, the null over the plume’s shape, and one call that vets a candidate. radtrax’s radtrax.retrieval.absorption is the plume-agnostic absorption-shape test those pieces run: the gas’s window and transmittance, the pairing of two pixel sets, the ratio, the fit and DnormD_{\text{norm}}. Every function below is imported from one of the two; the theory is on the plume vetting page.

What you’ll learn:

  1. How to get the transmittance the fit needs from radtrax’s ForwardModel, and why a constant absorption coefficient biases the column low.

  2. How seeds, one-to-one pairing and the ratio spectrum cancel the surface.

  3. How the profile fit and DnormD_{\text{norm}} separate a plume from a confounder, and how a null from the same scene calibrates the score.

Background

For a pixel under a plume and a pixel outside it with the same surface, sun and atmosphere, the ratio of their radiances cancels everything but the plume and what the two surfaces do not share:

R(λ)=Lin‾(λ)Lout‾(λ)≈P(λ;c) E(λ;ΔX),Eb(ΔX)=yb(state+ΔX)yb(state)R(\lambda) = \frac{\overline{L_{\text{in}}}(\lambda)}{\overline{L_{\text{out}}}(\lambda)} \approx P(\lambda; c)\, E(\lambda; \Delta X), \qquad E_b(\Delta X) = \frac{y_b(\text{state} + \Delta X)}{y_b(\text{state})}

with EE methane’s band transmittance for a column enhancement ΔX\Delta X [mol/m²] and PP a low-order Legendre series that absorbs the surface mismatch. The fit gives ΔX^\widehat{\Delta X}, and the score divides out the continuum, q=R/Pq = R / P, and compares qq with EE relative to qq’s own spread:

Dnorm=mean⁡λ∣q−E(ΔX^)∣mean⁡λ∣q−qˉ∣D_{\text{norm}} = \frac{\operatorname{mean}_\lambda \lvert q - E(\widehat{\Delta X}) \rvert}{\operatorname{mean}_\lambda \lvert q - \bar q \rvert}

A perfect fit gives 0; no methane gives ΔX^=0\widehat{\Delta X} = 0, E=1E = 1 and Dnorm≈1D_{\text{norm}} \approx 1. EMIT’s daily digest alerts below 0.3.

Python implementation: CPython
Python version       : 3.13.17
IPython version      : 9.10.0

numpy  : 2.4.6
scipy  : 1.17.1
jax    : 0.11.0
equinox: 0.13.8
radtrax: 0.0.0
plumax : 0.1.2

Compiler    : Clang 23.1.3 
OS          : Linux
Release     : 6.18.44-fc-v165
Machine     : x86_64
Processor   : x86_64
CPU cores   : 2
Architecture: 64bit

1. The transmittance the fit needs

Nine EMIT-like bands (8.5 nm wide, every 7.5 nm, 2296–2356 nm) on radtrax · 3’s synthetic table of 150 Voigt lines, so the notebook runs without HITRAN. The model is AFGL US Standard to 20 km with CH₄ and a stand-in H₂O, a 35° sun and a 10° view. radtrax’s transmittance_table puts a column enhancement ΔX\Delta X in the bottom layer and divides by the plume-free radiance; linear=True gives the reference code’s older model, exp⁡(−εΔX)\exp(-\varepsilon \Delta X) with ε=−∂ln⁡y/∂ΔX\varepsilon = -\partial \ln y / \partial \Delta X at 0, which is the matched filter’s target from radtrax’s unit_absorption_target.

1 ppm·m = 4.030e-05 mol/m² at the surface layer

The table is computed in 8 chunks of about 100 columns, each with its own background run at ΔX=0\Delta X = 0: one pass over all 801 holds several GB of per-layer, per-wavenumber intermediates on the 14,000-wavenumber grid. A fit of the exact transmittance with a constant coefficient returns too small a column, because lines saturate: a strong plume absorbs less per molecule than the weak-absorption slope predicts.

ΔX = 0.05 mol/m² (1,241 ppm·m): constant ε gives -1.4 %
ΔX = 0.10 mol/m² (2,481 ppm·m): constant ε gives -2.8 %
ΔX = 0.30 mol/m² (7,444 ppm·m): constant ε gives -7.7 %
deepest band absorption at 0.1 mol/m²: 1.8 %

2. The scene and the matched filter

A 64 × 64 scene: 30 channels outside methane’s band (1000–2050 nm) that carry only the surface, and the 9 absorbing bands. Three surface classes in patches, with a brightness texture of a few percent. Candidate A is a Gaussian plume from a point source; its mask is where the truth exceeds about 500 ppm·m. Candidate B is a plume-shaped ellipse of a carbonate-like surface whose reflectance dips by 6 % around 2335 nm. Gaussian noise at SNR 300 per channel. Radiance is reflectance times the unit-albedo forward model, Lb=ρb yb(0) Eb(ΔX)L_b = \rho_b\, y_b(0)\, E_b(\Delta X), interpolated in ΔX\Delta X from the table.

MF noise off the candidates: 484 ppm·m
A: 748 pixels, mean MF 593 ppm·m, peak 7,135 ppm·m, mean truth 1,035 ppm·m
B: 111 pixels, mean MF 1,162 ppm·m, peak 2,555 ppm·m, mean truth 11 ppm·m
<Figure size 1200x500 with 3 Axes>

The matched filter flags both, and scores the carbonate patch B higher on average than the plume A, whose mask includes the faint tail.

3. Vetting the two candidates

radtrax’s AbsorptionWindow here fits the nine absorbing bands and pairs on the 30 others. vet_candidate takes the seeds (select_seeds), the background pool (background_pool: every pixel within 16 pixels of the seeds’ bounding box, outside both candidates, with a matched-filter score under twice its noise), pairs the two sets one to one with radtrax’s pair_pixels, and fits the ratio.

A: ΔX̂ = 1,741 ppm·m, D_norm = 0.14, 64 pairs
B: ΔX̂ = 0 ppm·m, D_norm = 1.00, 54 pairs

A fits as methane with DnormD_{\text{norm}} well below 0.3. B fits at ΔX^=0\widehat{\Delta X} = 0: the continuum takes the carbonate’s smooth dip, nothing methane-shaped is left, and Dnorm≈1D_{\text{norm}} \approx 1. A’s column is below the truth over its seed pixels for two reasons: the fit returns the column of the mean transmittance, not the mean column (the transmittance is convex in ΔX\Delta X), and paired surfaces never match exactly.

A: truth over its kept pixels 1,865 ppm·m; the fit recovers 93 %
A with a constant coefficient: 1,691 ppm·m

4. A null from the same scene

The honest null is this scene’s clutter with no plume. null_masks draws 100 copies of each mask, rotated and translated off both candidates; each is vetted exactly as the candidate, and null_pvalue gives p^\hat p, the fraction that score at or below it.

A: p̂ = 0.00; null D_norm 5th percentile 0.47, median 0.79, 0 of 100 below 0.3
B: p̂ = 0.45; null D_norm 5th percentile 0.49, median 1.00, 1 of 100 below 0.3
<Figure size 1200x800 with 4 Axes>

Summary

  • The matched filter scored the carbonate patch above the plume; the spectral fit reversed the order, accepting A and finding no methane in B.

  • The transmittance comes from radtrax’s forward model for the scene’s own geometry and water vapour, so the curve of growth is built in; a constant coefficient biases strong plumes low.

  • DnormD_{\text{norm}} thresholds depend on the window and the continuum’s degree (this scene has 9 bands, EMIT’s window 46), so report the scene-null p^\hat p with every score and set the threshold per sensor on real scenes.

  • Vetting’s ΔX^\widehat{\Delta X} is a score, not a retrieval: the column for an emission rate comes from a per-pixel retrieval of the vetted plume.