The Beer–Lambert column retrieval
radtrax · 1 · From top-of-atmosphere reflectance to a methane column enhancement, term by term
Abstract¶
A methane plume over a bright surface removes a little light in the short-wave infrared, and the single-layer Beer–Lambert model turns that deficit into a column mass. This page writes the model out term by term, says where each term comes from in a satellite product, and follows the two ways to estimate the plume-free baseline: a proxy band for multispectral sensors such as Sentinel-2, and a continuum fit for hyperspectral ones such as EMIT. It then inverts the model with a look-up table. The source note’s physics holds. Five of its details do not: Sentinel-2’s band 11 is not blind to methane, the effective cross-section is valid only for weak absorption, Sentinel-2 L1C has a radiometric offset, Landsat L1 is not NetCDF or HDF5, and the operational hyperspectral retrievals are matched filters, not continuum fits. On a synthetic Sentinel-2 scene, the note’s recipe recovers 80 % of the plume’s mass; the multi-band ratio with a look-up table recovers 96 %. radtrax, the GeoModels package for radiative transfer in JAX (formerly rtmx), would own the band transmittance, the table and its inversion. Much of it exists already in plumax (band-integrated radiative transfer, a HITRAN table pipeline, a matched filter), which the package boundaries page proposes to move.
About this page
| Status | Design. radtrax does not exist yet; this page fixes the physics its first module implements, and names what the stack and plumax already do |
| Reading time | about 25 minutes |
| Audience | anyone writing or reviewing a methane column retrieval, and anyone deciding what radtrax owns |
| Companions | radtrax · 2 (the multi-pass family and the MBSP estimator); radtrax · 3 (radtrax’s design); plumax · 1 (the emission rate from the column); the package boundaries page (where the code moves); Report 16 (one methane scene through geotoolz, with the matched filter and the IME); Report 9 (the use cases built on the same scene); Report 5 (pipelines under JAX, for a differentiable radtrax); Report 17 (the five-band sensor, a simpler Beer–Lambert model) |
| Code checked against | georeader-spaceml 2.4.1 (the Sentinel-2 L1C reader, its SRF table COPE-GSEG-EOPG-TN-15-0007 v4.0, the EMIT reader and the Gaussian SRF helper); geotoolz 0.10.2 at 088475e (plume, spectral, matched_filter); xrtoolz 0.0.5 at 308c7f2 (atm.gas.ch4). Read on 14 October 2026 |
| Running example | every number and figure is from one run of notebooks/geomodels/radtrax_01_column_retrieval.py, which builds the scenes from fixed seeds and asserts each number the page shows; code comments carry the status markers |
| Sources | the source note, “The Core Forward Model Equation” and its four companion sections Johnson, 2026; Varon et al. on Sentinel-2 Varon et al., 2021; Thompson et al. on the matched filter Thompson et al., 2015 |
The forward model¶
The reflectance a sensor measures in band at pixel is the surface reflectance times the transmittance of the two-way path through the atmosphere, averaged over the band’s spectral response :
Here is the methane cross-section per unit mass, the background column and the plume’s enhancement. When every line is optically thin, the band average of the exponential is the exponential of the band-averaged cross-section, and (1) becomes the source note’s model:
so that the plume’s optical depth is linear in the column:
(2) is the right picture for small plumes and the wrong one for strong lines. The running example measures how wrong in stage 2.
The variables¶
Table 1 gives each term of (1), its units and the piece of the stack that supplies it.
Table 1:Every term of the model, where it comes from, and the stack piece that supplies it today.
| Symbol | Name | Role | Units | From | In the stack today |
|---|---|---|---|---|---|
| top-of-atmosphere reflectance | input | dimensionless | the L1 product | georeader’s Sentinel-2 L1C and EMIT readers | |
| surface reflectance | nuisance | dimensionless | scene statistics, as part of the baseline | ||
| background transmittance (methane, water vapour, CO₂) | nuisance | dimensionless | scene statistics, or a radiative transfer model | ||
| two-way geometric air-mass factor | input | dimensionless | sun and view zenith angles in the metadata | georeader’s sza and vza grids (Sentinel-2) | |
| , | mass absorption cross-section, and its band average | parameter | m²/kg | a line list, then the band’s response | georeader’s read_srf (Sentinel-2) and srf (Gaussian); plumax’s hapi_lut (HITRAN through HAPI) and radtran.srf |
| background methane column | parameter | kg/m² | climatology or a model | xrtoolz’s dry_air_column and mixing_ratio_to_column | |
| column mass enhancement | output | kg/m² | the inversion | geotoolz’s matched filter; xrtoolz’s column_mass_to_delta_vmr converts it |
Where each term comes from¶
Each term has one source in a satellite product (Figure 1).
Figure 1:The sources of the model’s terms, from the notebook.
Reflectance. For Sentinel-2 L1C the granule’s JPEG 2000 bands are top-of-atmosphere reflectance scaled to integers:
with a quantification value of 10,000 and, since processing baseline 04.00 (January 2022), an offset of −1,000. Hyperspectral L1 products are radiance , converted with the band’s solar irradiance and the Earth–Sun distance in astronomical units:
Air-mass factor. The geometric two-way path through a plane-parallel layer is
with and the sun and view zenith angles.
Cross-section. A line list gives at the layer’s pressure and temperature; the band’s response function averages it, (2), or, for the exact model, averages the transmittance, (1).
1 2 3 4 5 6 7 8 9 10 11# Shapes: S2 L1C tile (13, H, W); EMIT L1B (285, H, W); SRF table (877, 13) s2 = S2ImageL1C(safe_path) # today s2.quantification_value(), s2.radio_add_offsets() # 10000, {B12: -1000} today # R_b = (DN_b + offset_b) / Q R = s2.load() # reflectance today # m = 1/cos θ_s + 1/cos θ_v, from the 5 km angle grids sza, vza = s2.sza, s2.vza["B12"] # GeoTensors today m = 1 / np.cos(np.radians(sza)) + 1 / np.cos(np.radians(vza)) # user # R_b = π L_b d² / (E_b cos θ_s) emit = EMITImage(l1b_path); R_h = emit.load(as_reflectance=True) # today srf = read_srf("S2A") # B11, B12 at 1 nm, offline today
Reading the terms with the stack today.
The running example: one plume, two sensors¶
A 4 km × 4 km Sentinel-2 scene at 20 m, 200 × 200 pixels. The surface has a smooth band 11 reflectance between 0.20 and 0.40 and two cover types, with band 12 at 0.84 and 0.80 of band 11 and a 1 % per-pixel scatter. A Gaussian plume, elongated and rotated by 30°, peaks at kg/m². The background is 1.9 ppm, a column of 0.0109 kg/m², and the sun and view zenith angles are 35° and 5°, so (6) gives . Each band adds noise with a standard deviation of 0.0015 in reflectance. Stage 5 adds an EMIT-like cube.
Stage 1 · The forward model (georeader → radtrax)¶
TL;DR. Build the exact model (1) with Sentinel-2’s own response functions.
1 2 3 4 5 6 7 8 9 10# Shapes: G = 100,000 wavelengths (0.01 nm); H = W = 200 srf = read_srf("S2A") # (877, 13) at 1 nm today f11, f12 = np.interp(WL, srf.index, srf["B11"]), ... # (G,) user # σ̄_b = ∫ f_b σ dλ / ∫ f_b dλ s11, s12 = band_mean(sigma, f11), band_mean(sigma, f12) # 0.04, 0.40 user # T_b(m, Ω) = ∫ f_b exp(−m σ Ω) dλ / ∫ f_b dλ T = radtrax.band_transmittance(sigma, f12) # (m, Ω) → T proposed (radtrax) # R_b = ρ_b T_b(m, Ω_bg + ΔΩ) + ε_b R12 = rho12 * T(m, OMEGA_BG + d_omega) + eps # (H, W) user # T₁₂(m, Ω_bg) = 0.990: the background takes 1 % of band 12
The forward model with Sentinel-2A’s band 11 and band 12, from a real run.
Sentinel-2A’s band 12 is above half its peak from 2113 to 2286 nm, and band 11 reaches 1668 nm. The background methane already removes 1 % of band 12’s light along this path.
Stage 2 · The effective cross-section against the exact band (radtrax)¶
TL;DR. Compare the linear optical depth (3) with the exact one, the curve of growth.
Problem. The plume’s exact band optical depth, above the background, is
and the linear model claims . A line’s core saturates long before its wings do, so the band absorbs less than the linear model says.
Figure 2:The curve of growth for band 12 and band 11, on the synthetic spectrum.
The exact optical depth (7) falls short of the linear one (Figure 2) by 3.8 % at 0.05 kg/m², 15 % at the plume’s peak (0.3 kg/m², where band 12 dips by 20 %, not the 23 % the linear model gives), 22 % at 0.5 and 36 % at 1 kg/m². Band 11 absorbs about a tenth as much, which is the ratio this spectrum was built with.
Stage 3 · The multispectral proxy baseline (geotoolz, radtrax)¶
TL;DR. Estimate the plume-free band 12 from band 11 two ways, invert linearly, and integrate the plume’s mass.
Problem. Under the plume, is unknown. Band 11 sees the same surface. (a) The source note fits on pixels outside a suspected-plume mask and uses as the baseline. (b) Varon et al.'s multi-band single-pass (MBSP) retrieval Varon et al., 2021, whose estimator radtrax · 2 derives, fits through the origin over the whole scene and uses the fractional change
to first order, which keeps band 11’s own absorption. Both are scored by the plume’s mass above the median of a ring of clean pixels around it, as the integrated mass enhancement method does.
1 2 3 4 5 6 7 8 9 10# Shapes: H = W = 200; plume mask 3,777 pixels; true mass 108,979 kg # (a) R₁₂ ≈ k R₁₁ + b on clean pixels; τ = −ln(R₁₂ / (k R₁₁ + b)) k, b = np.polyfit(R11[~suspect], R12[~suspect], 1) # 0.816, 0.0016 user omega_a = -np.log(R12 / (k * R11 + b)) / (m * s12) # B11 "blind" user # → 80 % of the mass # (b) R₁₁ ≈ c R₁₂ through the origin; ΔΩ = −ln(c R₁₂ / R₁₁) / (m (σ̄₁₂ − σ̄₁₁)) c = (R11 * R12).sum() / (R12 * R12).sum() # 1.220 user omega_b = -np.log(c * R12 / R11) / (m * (s12 - s11)) # user # → 89 % of the mass; noise off the plume 0.029 (a), 0.032 (b) kg/m² gz.plume.SBMP()(scene, reference_scene) # the multi-pass ratio today
Two proxy baselines on the Sentinel-2 scene, from a real run.
The note’s recipe loses a fifth of the plume. Two causes add: band 11 absorbs under the plume, so the baseline is too low there by about of the signal, a tenth here; and saturation (stage 2) takes most of the rest. MBSP removes the first by subtracting , and recovers 89 %. Neither baseline can model the second surface cover: its patches, where band 12 is 0.80 of band 11 rather than 0.84, come out as false enhancements of 0.04 kg/m² (Figure 3). That surface term, not the sensor noise, sets the noise floor.
Stage 4 · A look-up table of the band observable (radtrax)¶
TL;DR. Tabulate the MBSP observable exactly over air-mass factor and enhancement, and invert each pixel by interpolation.
Problem. Replace the first-order relation in (8) by the exact band observable
tabulated once, and solve for in each pixel.
1 2 3 4 5 6 7 8# Shapes: table (7, 81) over m ∈ [2, 3.5] and ΔΩ ∈ [0, 0.4] kg/m² # g(m, ΔΩ) from the exact band transmittances, both bands lut = radtrax.Table(observable="mbsp", sensor="S2A", m=np.linspace(2, 3.5, 7), omega=np.linspace(0, 0.4, 81)) proposed (radtrax) # ΔΩ = g⁻¹(−ln(c R₁₂ / R₁₁); m), odd-extended for negative noise omega_lut = lut.invert(-np.log(c * R12 / R11), m) # (H, W) proposed (radtrax) # → 96 % of the mass; RMSE over the plume 0.0175 kg/m² (linear: 0.0206)
Building and inverting the table, from a real run.
Figure 3:The truth, the source note’s recipe and MBSP with the table.
The table recovers 96 % of the mass. The remaining 4 % comes from the ring’s median, which the surface patches pull below zero. Because of (9) is increasing in at every in the table, the inversion is a one-dimensional interpolation. The note’s table has water vapour as a second index. That matters where water and methane lines overlap in band 12; this synthetic spectrum has no water, so it does not test it.
Stage 5 · Hyperspectral: a continuum against a matched filter (georeader, geotoolz)¶
TL;DR. On an EMIT-like cube, fit the note’s quadratic continuum and compare it with geotoolz’s matched filter, over a surface with a carbonate absorption.
Problem. A 120 × 120 cube with 68 bands from 2000 to 2500 nm, 7.4 nm apart with an 8.5 nm full width at half maximum. Surfaces follow a quadratic in wavelength, and 15 % of pixels carry calcite’s absorption near 2340 nm, inside the methane band. The plume peaks at 0.15 kg/m². (a) The note’s continuum: a quadratic per pixel through the 2120–2150 and 2430–2470 nm windows, then a least-squares column over the 27 bands from 2200 to 2400 nm. (b) The matched filter Thompson et al., 2015, with the linearised target for the background mean :
1 2 3 4 5 6 7 8 9 10 11 12# Shapes: cube (C, H, W) = (68, 120, 120); 9 window bands, 27 fit bands srf_h = reflectance.srf(centres, fwhm, WLH) # (G, C) Gaussian today # (a) R / (a λ² + b λ + c) fitted on the windows; ΔΩ by least squares omega_cont = continuum_column(cube, windows, fit) # (H, W) user # (b) Δ̂Ω = (x − μ)ᵀ Σ⁻¹ t / tᵀ Σ⁻¹ t, t = −μ ⊙ m σ̄ mf = gz.matched_filter.MatchedFilter(target=t, fit_on_call=True, axis=0) # today omega_mf = mf(cube) # background includes the plume today masked = cube.copy(); masked[:, suspect] = np.nan # NaN: left out of the fit mf_bg = gz.matched_filter.MatchedFilter(target=t, axis=0).fit(masked) # today omega_mf_bg = mf_bg.transform(cube) # today # mass: continuum 95 %, matched filter 72 % (plume in Σ), 91 % (masked) # over carbonate: continuum +0.0069, matched filter +0.0002 kg/m²
The continuum fit and geotoolz’s matched filter on the cube, from a real run.
Figure 4:The truth, the note’s continuum fit and the matched filter with the plume left out of its background.
The continuum fit recovers 95 % of the mass but reads carbonate as methane
(Figure 4):
pixels with calcite come out 0.0069 kg/m² high, against 0.0029 kg/m² low
over the rest, and its noise off the plume is 0.0095 kg/m². The matched
filter (10) learns the carbonate from the scene covariance and leaves no bias
there (0.0002 kg/m²), with a noise of 0.0016 kg/m². Its one trap is the
plume itself: with the plume inside the background statistics, the filter
recovers only 72 % of the mass; with the suspected plume masked out of the
fit, 91 %. The rest is saturation, as in stage 2, because the target is
linearised. At the peak, 0.15 kg/m² mixed over a 500 m layer is a 442 ppm
enhancement (xrtoolz’s column_mass_to_delta_vmr).
What radtrax would own¶
Where each piece is today is in the package boundaries inventory; Table 2 keeps what radtrax would hold.
Table 2:Each step of the retrieval and what radtrax would hold.
| Step | radtrax (proposed) |
|---|---|
| read reflectance, offsets, angles | stays in georeader |
| response functions | one band-integration operator, reading georeader’s tables through [geo] |
| cross-sections from a line list | moved from plumax hapi_lut |
| band transmittance | moved from plumax radtran.forward, with the background column of (1) |
| baselines | SBMP moved from geotoolz, and the single-pass MBSP observable (8) |
| table and inversion | Table over , monotone inversion, in place of plumax’s radtran.nb_lut |
| matched filter | one core, merged from the three |
| variational retrieval | moved from plumax assimilation, or split with vardax (open) |
| units, mass and rate | stay in xrtoolz; the plume operators go to plumax |
Written in JAX, the band transmittance is differentiable, so the table becomes optional: a Gauss–Newton step on (1) inverts each pixel directly, as Report 17’s Laplace step does on the five-band sensor, and a learned retrieval can be trained through it (Report 5).
Open questions¶
Are the note’s continuum windows clean? 2120–2150 and 2430–2470 nm need checking against HITRAN’s methane and water lines at EMIT’s resolution; the long-wave window sits at the edge of methane’s 2.3 μm band.
Which band 11 to band 12 ratio does Sentinel-2 really have? Stage 3’s bias scales with ; the tenth used here is a choice, and the real ratio follows from HITRAN and the two response functions.
A table or a solver? A table is fast and auditable; a differentiable forward model is exact at every pixel and composes with learning. radtrax could ship both, the table built from the forward model.
Which atmosphere? The single layer with a geometric air-mass factor ignores scattering and the vertical profile. When does a layered model pay for itself on Sentinel-2’s bands?
Upcoming work¶
Table 3:Follow-ups from checking the source note.
| Follow-up | Where | Why |
|---|---|---|
| Rerun the example on HITRAN lines | this page’s notebook, with a cached line list | the spectrum here is synthetic |
| Add the single-pass MBSP observable | radtrax (or geotoolz plume until radtrax exists) | stage 3: the stack has no single-pass proxy |
| Ship a Landsat L1 reader path with the MTL scaling and angle bands | georeader | the note’s Landsat route does not exist in the stack |
References¶
The source note on the forward model, provenance, proxies and look-up table Johnson, 2026.
Varon et al. on Sentinel-2 methane retrievals Varon et al., 2021, and on the integrated mass enhancement Varon et al., 2018.
Thompson et al. on the matched filter for imaging spectroscopy Thompson et al., 2015.
georeader-spaceml, geotoolz and xrtoolz at the revisions in the header.
- Johnson, J. E. (2026). The Core Forward Model Equation: a Beer–Lambert methane retrieval, its inputs, proxies and look-up table. Unpublished design note.
- Varon, D. J., Jervis, D., McKeever, J., Spence, I., Gains, D., & Jacob, D. J. (2021). High-frequency monitoring of anomalous methane point sources with multispectral Sentinel-2 satellite observations. Atmospheric Measurement Techniques, 14(4), 2771–2785. 10.5194/amt-14-2771-2021
- Thompson, D. R., Leifer, I., Bovensmann, H., Eastwood, M., Fladeland, M., Frankenberg, C., Gerilowski, K., Green, R. O., Kratwurst, S., Krings, T., Luna, B., & Thorpe, A. K. (2015). Real-time remote detection and measurement for airborne imaging spectroscopy: a case study with methane. Atmospheric Measurement Techniques, 8(10), 4383–4397. 10.5194/amt-8-4383-2015
- Varon, D. J., Jacob, D. J., McKeever, J., Jervis, D., Durak, B. O. A., Xia, Y., & Huang, Y. (2018). Quantifying methane point sources from fine-scale satellite observations of atmospheric methane plumes. Atmospheric Measurement Techniques, 11(10), 5673–5686. 10.5194/amt-11-5673-2018