radtrax: design and roadmap
radtrax · 3 · A differentiable radiative transfer package in JAX and Equinox, formerly rtmx — what to build, in what order, and what the stack already has
Abstract¶
radtrax is the radiative transfer package the stack does not have yet: a forward model from an atmospheric state to what a sensor measures, written in JAX and Equinox so that every Jacobian a retrieval needs comes from automatic differentiation rather than hand-derived code. It was called rtmx in the earlier design; the design itself is unchanged in intent. This page sets out the architecture (a typed pipeline from atmosphere to instrument, with the per-layer optical properties as the canonical differentiable interface), a staged roadmap from Beer–Lambert to multiple scattering, and the package’s place in the stack, and checks each against code that exists. Three findings shape it:
v0 is a port, not a start. plumax already has most of v0 on one slab; a layered Equinox v0 reproduces its radiance and hand-written Jacobian to 10-15, and one slab at the surface’s pressure misstates a band’s optical depth by up to 19 %.
The source note’s Equinox sketch needs three fixes: no JAX arrays in static fields (Equinox warns), no
AbstractVaron a concrete module, and optimistix and optax in place of jaxopt and optax-contrib.The stack has half of the v4 linear algebra. gaussx’s block-tridiagonal operator solves the boundary-value problem’s structure to 10-16, but only for a symmetric band; discrete ordinates needs a general one.
About this page
| Status | Proposal. radtrax does not exist; nothing here changes code. The package boundaries page decides what moves into it from plumax and geotoolz |
| Formerly | rtmx. The name changes to radtrax; the scope (differentiable SWIR radiative transfer for multi-gas retrievals) does not |
| Reading time | about 35 minutes |
| Audience | whoever builds radtrax, and anyone deciding whether a retrieval should wait for it |
| Companions | radtrax · 1 (the single-pass column retrieval v0 must reproduce); radtrax · 2 (the Sentinel-2 multi-pass family); package boundaries (the moves and the dependency rules); plumax · 1 (the emission rate downstream of the column) |
| Code checked against | plumax 0.1.2 at 28f257e, gaussx 0.6.5 at 7fa1a38, geotoolz 0.10.2 at 088475e, georeader-spaceml 2.4.1, finitevolX at 32ffb45 and spectraldiffx at b5f3dc4 from references/; pyrox at 94eef13, somax at aea3a03; equinox 0.13.8, lineax 0.1.1, optimistix 0.1.0, optax 0.2.8. Read on 14 October 2026 |
| Running example | every number is from one run of notebooks/geomodels/radtrax_03_design.py, which asserts each one, on a synthetic table of 150 Voigt lines near 4300 cm⁻¹ (2.33 μm); code comments carry the status markers |
| Sources | the design note supplied by the author Johnson, 2026; Korkin et al. Korkin et al., 2022; ExoJAX Kawahara & others, 2022; VLIDORT Spurr, 2006; DISORT Stamnes et al., 1988; Rodgers Rodgers, 2000 |
What radtrax is for¶
TL;DR. One forward model, every derivative of it, and four retrievals that need them.
Problem. A retrieval inverts for the state
(trace-gas profiles, temperature, surface, instrument), and every serious
inversion needs : optimal estimation iterates
on it Rodgers, 2000, a matched filter’s target spectrum is one column of
it, and an emission-rate fit chains it with a plume model. The heritage
codes either ship by hand-derived linearisation (VLIDORT Spurr, 2006)
or not at all (MODTRAN tables). radtrax’s one bet is to make
, the per-layer optical
depth, single-scattering albedo, phase-function moments and surface
kernels, the interface between spectroscopy and solvers, and to get
through it by jax.jacrev. Its uses, in the order the roadmap reaches
them:
Matched-filter targets for EMIT, PRISMA, EnMAP and AVIRIS-NG: the unit absorption spectrum for a scene’s geometry and water vapour, replacing a MODTRAN table; it is the of the matched filter in radtrax · 1 and Report 16.
Optimal-estimation retrievals of , , surface and spectral shift, with from autodiff.
Multispectral retrievals (Sentinel-2, Landsat): the band tables and the multi-pass family of radtrax · 2.
Emission rates by maximum likelihood: plumax maps an emission to a column, radtrax maps the column to radiance, and one gradient gives ; the IME and the cross-sectional flux of plumax · 1 become its starting point.
What exists today¶
TL;DR. Most of v0 is in plumax already, in NumPy, on one slab.
The package boundaries inventory lists every module and its size; the table keeps what radtrax changes in each.
| Piece | Today | What radtrax changes |
|---|---|---|
| HITRAN cross-section tables | plumax hapi_lut: HAPI, offline, on a (ν, T, p) grid | kept as the offline table builder behind radtrax[hitran] |
| Beer–Lambert forward model | plumax radtran.forward: NumPy, one slab at one (p, T), Jacobian by hand | Equinox, layered, Jacobian by autodiff |
| Band integration, band tables | plumax radtran.srf, radtran.nb_lut; georeader read_srf: nB without the background column (radtrax · 2) | one JAX Instrument; tables with the background |
| PSF, ground sampling | plumax radtran.instrument: NumPy, hand-written adjoints | JAX, adjoint by autodiff, the hand adjoints kept as tests |
| Matched filters | three implementations | one JAX core |
| Observation operator, 3D-Var | plumax assimilation: JAX, frozen dataclass, linear in , one slab | the v1 retrieval drivers, on the nonlinear layered model |
| Structured linear algebra | gaussx (LowRankUpdate, BlockTriDiag, solve) | used as is |
The pieces radtrax needs are present; what is missing is the layer dimension, a single Equinox forward model they share, and everything past Beer–Lambert.
The architecture¶
TL;DR. Five stages, typed pytrees between them, and protocols where the alternatives plug in.
AtmosphericState p, T, z per layer; VMR per gas (L,), dict
│ OpacityProvider: σ(ν; p_l, T_l) per gas, Σ n x σ Δz
▼
OpticalProperties τ (L, G), ω (L, G), χ (L, G, K) ← K flows here
│ + Surface: Lambertian | RossLi | RPV | CoxMunk
│ RTSolver: BeerLambert | SingleScatter | TwoStream
│ | DiscreteOrdinates(N) | DoublingAdding
▼
Radiance L(ν) at the top of the atmosphere (G,)
│ Instrument: SRF, spectral shift, FWHM, PSF, GSD
▼
Observation y_b per band; NoiseModel → Σ_y (B,)
│ Retrieval: OE (optimistix LM / GN) | MAP (optax)
│ | Bayesian (numpyro) | matched filter
▼
State estimate x̂, Ŝ, averaging kernelFour rules come out of the note and the checks below:
Modules carry state; functions carry transforms. A table, a line list, an SRF or a noise covariance is an
eqx.Module; a Voigt profile, a Chapman factor or a delta-M scaling is a function.Arrays are leaves. A JAX array in a static field makes Equinox warn and breaks hashing under
filter_jit. Grids, tables and SRFs stay leaves; what is not differentiated is chosen witheqx.partitionorjax.lax.stop_gradient.Protocols at the seams.
OpacityProvider,RTSolver,Surface,InstrumentandNoiseModelare abstract Equinox modules (eqx.AbstractVarandeqx.abstractmethodon the base, concrete fields on the subclass), sofilter_jitcompiles each combination.vmapoutsidejacrev. A swath is afilter_vmapover pixels of a per-pixel Jacobian, never a Jacobian of a batch.
The proposed layout:
radtrax/
├── atmosphere/ AtmosphericState, AFGL profiles, hydrostatics
├── spectroscopy/ tables (v0), line-by-line (v2): partition functions,
│ line strengths, Voigt, continua, CIA
├── optics/ OpticalProperties, Rayleigh, HG and Mie, delta-M
├── surface/ Lambertian, kernel BRDFs, Cox–Munk
├── solvers/ BeerLambert, SingleScatter, TwoStream, ...
├── instrument/ SRF, shift and FWHM, PSF, GSD, noise
├── retrieval/ optimal estimation, matched filter, multi-pass
└── geo/ GeoTensor wrappers, sensor SRF tables [geo] extraThe v0 slice, run¶
TL;DR. A layered Equinox forward model reproduces plumax’s slab exactly, and shows what the slab gets wrong.
Problem. The smallest radtrax that is worth having is the note’s v0: an (ln p, T) table lookup per layer, Beer–Lambert along the two-way path, and a band integration with a spectral shift. The notebook writes it with every array a leaf:
1 2 3 4 5 6 7 8 9 10 11 12 13# Shapes: L = 20 layers, G = 14,000 wavenumbers, B = 7 bands (8.5 nm) # σ_l = bilinear(σ; ln p_l, T_l); τ_l = σ_l n_l x_l Δz_l (L, G) tau = TableOpacity(log_p, t_grid, sigma)(atm) # proposed (radtrax) # L = (μ₀/π) A exp(−m Σ_l τ_l), m = 1/μ₀ + 1/μ (G,) rad = BeerLambert()(tau, albedo, mu0, mu) # proposed (radtrax) # y_b = Σ_g f_bg L(ν_g + δ) (B,) y = Instrument(srf, nu, shift)(rad) # proposed (radtrax) # one slab against plumax: L and ∂L/∂x (G,) ref = forward_nonlinear(ds, nu, T_K=T0, p_atm=P0, vmr=X0, ...) # today # → radiance to 1.3 × 10⁻¹⁵, Jacobian to 6 × 10⁻¹⁶, relative user # K = ∂y/∂(ΔX₀, A, δ) per pixel, 256 pixels (256, 3) → (256, 7, 3) K = eqx.filter_vmap(jax.jacrev(y_of))(thetas) # user # → finite differences to 2.3 × 10⁻⁸; vmap equals a loop exactly
The corrected v0 forward model and its Jacobian, from a real run.
The layered model reduces to plumax’s slab, radiance and Jacobian alike,
to rounding; the Jacobian that plumax derives by hand comes out of
jax.jacrev unchanged. The layers are not decoration. The same 1.9 ppm
column ( molecules/cm²) through 20 one-kilometre layers
and through one slab at the surface’s pressure differ by up to 19 % in a
band’s optical depth, because the slab broadens every line as if all the
methane sat at 1 atm; a slab at the column’s pressure-weighted mean
differs by up to 3 %. plumax’s forward model and its observation
operator are both single-slab, which is the first thing v0 changes.
The roadmap¶
TL;DR. Six releases, each a working retrieval, each adding one fidelity axis, never a refactor and a feature together.
Table 1:The releases, what each ports and adds, and the check that closes it. Durations are the note’s estimates.
| Release | Ports from the stack | Adds | Delivers | Closed when |
|---|---|---|---|---|
| v0 Beer–Lambert, 4–6 weeks | plumax hapi_lut (tables), radtran.forward, srf, nb_lut, target | layers; one Equinox forward model; band tables with the background; tables for CH₄, H₂O, CO₂, N₂O, CO | matched-filter targets with their gradients | slab equals plumax to 10-12 (done here); transmittance within 0.1 % of HAPI on AFGL US Standard; against finite differences |
| v1 geometry, surface, instrument, noise, 4–6 weeks | plumax assimilation, radtran.instrument; geotoolz plume.SBMP (an MBMP) and the MBSP of radtrax · 1 | Chapman air mass above 70°; kernel BRDFs; SRF shift and FWHM; noise covariance; the multi-pass family of radtrax · 2 | Rodgers OE for , , surface, shift | EMIT plume scenes against the operational product |
| v2 line-by-line, 8–12 weeks | none | Voigt (direct), DIT/FFT and precomputed line-basis opacity; partition functions; continua (MT_CKD), CIA, line mixing | tables built in JAX, differentiable in the line parameters | 0.1 % against HAPI and LBLRTM on three AFGL atmospheres; single lines to 10-6 |
| v3 single scattering, 8–10 weeks | none | Rayleigh with King factor; HG and double HG; Mie on a precomputed grid; analytic single scatter with a TMS correction | aerosol-aware SWIR forward model | Coulson tables, miepython, 6SV |
| v4 multiple scattering, 12–16 weeks | gaussx operators | two-stream and Toon adding; doubling–adding; discrete ordinates with implicit differentiation | TROPOMI-class SWIR + O₂ A-band forward model | DISORT and VLIDORT to 0.5 % in radiance and 2 % in |
| v5+ | pyrox | polarisation; Cox–Munk; 3-D; emulators behind the RTSolver protocol | operational throughput | I3RC; an emulator’s error budget |
Each release in Table 1 ships the regression set of the one before; the gradient check, autodiff against central finite differences on a reduced state, is the one test every release runs.
Differentiability, component by component¶
TL;DR. Most of radtrax differentiates as written; four places do not, and the stack has some of the fixes.
Problem. Reverse mode through jnp code is exact where the code is
smooth. The places it is not are known from the heritage codes, and the
notebook reproduces each one:
1 2 3 4 5 6 7 8 9 10 11# (a) float32 on a wavenumber grid, near 4300 cm⁻¹ np.spacing(np.float32(4300.0)) # → 4.9e-4 cm⁻¹ (Doppler HWHM 6.6e-3) # (b) M(μ) = (e^{−τ/μ₀} − e^{−τ/μ}) / (1/μ − 1/μ₀) at μ = μ₀ () → () multiplier_naive(mu0) # → nan, and its gradient nan user multiplier_safe(mu0) # Taylor branch, double-where user # → τ e^{−τ/μ₀} exactly; slope = finite differences to 10⁻⁴ # (c) ∂/∂A of eigenvectors, repeated eigenvalue (3, 3) → (3, 3) jax.grad(first_eigvec_sum)(diag(1, 1, 2)) # → not finite user jax.grad(first_eigvec_sum)(diag(1, 1.001, 2)) # → 500, as 1/gap user # (d) block-tridiagonal solve, 30 blocks of 8 (240,) → (240,) gx.solve(gx.BlockTriDiag(D, A), b) # → dense solve to 5e-16 today
The four places autodiff needs help, from a real run.
Precision. Line-by-line opacity runs in float64. In float32 the wavenumber grid itself is coarse: cm⁻¹ at 4300 cm⁻¹, 7 % of the Doppler half-width, so line centres move by a visible fraction of a line.
Small denominators. The discrete-ordinates streaming multipliers are 0/0 at , in value and in gradient. VLIDORT switches to a Taylor series within Spurr, 2006; in JAX that branch needs the double-where (a safe denominator in the branch not taken) or
jnp.wherestill propagates the NaN into the gradient.Repeated eigenvalues.
jnp.linalg.eighgradients of eigenvectors scale as one over the eigenvalue gap and are not finite at a repeated one. Discrete ordinates’ reduced eigenproblem Stamnes et al., 1988 needs acustom_jvpthat differentiates the invariant subspace, not the individual vectors.The boundary-value solve. Discrete ordinates ends in one block-tridiagonal system per Fourier component; implicit differentiation through it costs one more solve. gaussx’s
BlockTriDiagsolves that structure in and matches a dense solve to , but its band is symmetric ( below, above), and withsymmetric=Falseit falls back to a dense LU. The boundary-value matrix is not symmetric, so v4 needs a general block-tridiagonal solve, which belongs in gaussx.
Fixed points (successive orders, SHDOM-like iterations) and the
Gauss–Newton and Levenberg–Marquardt loops of optimal estimation use
optimistix (FixedPointIteration, GaussNewton, LevenbergMarquardt,
with ImplicitAdjoint), which plumax’s assimilation already depends on;
L-BFGS for MAP retrievals is optax.lbfgs.
Dependencies and boundaries¶
TL;DR. radtrax depends on GeoNumerics and nothing above it; geo carriers arrive through an extra; plumax imports radtrax.
| Depends on | Why | How |
|---|---|---|
| jax, equinox, jaxtyping | the forward model and its pytrees | core |
| lineax, optimistix | linear solves, fixed points, OE drivers, implicit adjoints | core |
| gaussx | structured covariances (matched filter, OE), block-tridiagonal solves | core |
| hitran-api (HAPI), radis | offline line lists and tables | radtrax[hitran] |
| georeader, geotoolz | GeoTensor wrappers, sensor SRF tables | radtrax[geo] |
| numpyro, optax | Bayesian and MAP retrievals | radtrax[bayes] |
| pyrox | GP emulators behind the RTSolver protocol | radtrax[emulate] |
What to inherit from the landscape¶
The note surveys the field; the table keeps what each code contributes to radtrax’s design. Entries marked † were checked for this page (the class names against ExoJAX’s documentation, the status against JAXopt’s README, the papers against their publication records); the rest are the note’s.
| Code | Contributes | Where in radtrax |
|---|---|---|
| ExoJAX† Kawahara & others, 2022 | the closest JAX design: OpaDirect, OpaModit, OpaPremodit opacities; ArtEmisPure, ArtEmisScat, ArtTransPure, ArtReflectPure solvers; layer-wise scan for memory | v2 spectroscopy; v4 two-stream; plain JAX, which radtrax re-frames in Equinox |
| VLIDORT Spurr, 2006 | analytic K matrices in , , phase moments and surface; Taylor branches for small denominators | the K-matrix interface; v4 tests |
| DISORT Stamnes et al., 1988 | the reduced eigenproblem; exponential scaling; delta-M and TMS | v4 discrete ordinates |
| ARTS | workspace and agendas: typed state, rewireable pipelines | the module DAG |
| libRadtran | one optical setup, many solvers | the RTSolver protocol |
| py4CAtS | typed stages: cross section → absorption → optical depth → radiance | the pytree chain |
| RTTOV | forward, tangent linear, adjoint and K as one model | jvp, vjp, jacrev of one function |
Korkin et al.† Korkin et al., 2022 (gsit) | make it right, then fast; Fourier azimuth modes as a vmap axis; analytic single scatter subtracted and added back | v3 and v4 structure |
| PyMieDiff† (arXiv 2512.08614) | autograd-safe Bessel recurrences, in PyTorch | v3 Mie |
| sRTMnet, ISOFIT | emulated radiative transfer in operations | v5 emulators |
Open questions¶
Where does the 3D-Var retrieval live? plumax’s
assimilationis radtrax’s v1 driver in all but name; the package boundaries page asks whether it belongs in radtrax or in vardax, and v1 needs the answer.Tables or line-by-line as the default? v0’s tables are fast and differentiable in the state; v2’s line-by-line is differentiable in the line parameters too, which is what HITRAN-uncertainty propagation needs. Whether retrievals default to v2 once it exists is a cost question.
How general must the block-tridiagonal solve be? A non-symmetric
BlockTriDiagin gaussx serves discrete ordinates and any other two-point boundary-value problem; it is the one GeoNumerics addition v4 needs.Which sensor first for v1? EMIT has operational products to compare against; Sentinel-2 has the multi-pass family already written down.
Upcoming work¶
Table 2 lists the steps, in order.
Table 2:What radtrax needs, in order.
| Step | Where | Why |
|---|---|---|
| Create the radtrax repository from the package template, with the layout above | new repo | the package does not exist |
Port hapi_lut, radtran and assimilation’s observation operator into one layered Equinox forward model | radtrax v0, from plumax | v0 slice: plumax’s slab, its hand Jacobian and a 19 % slab error |
| Build band tables against the background column | radtrax v0 | radtrax · 2: 1.1 % on band 12 |
Add a non-symmetric BlockTriDiag | gaussx | differentiability, item 4: discrete ordinates’ boundary-value matrix is not symmetric |
| Add the gradient check (autodiff against finite differences) to every public forward function’s tests | radtrax | the one test every release runs |
| Update the package boundaries graph with radtrax’s extras | geostack docs | [hitran], [bayes] and [emulate] are new edges |
References¶
The design note, “A differentiable JAX/Equinox RTM for methane and multi-gas remote sensing — design and staged roadmap” Johnson, 2026.
Korkin et al., the practical guide Korkin et al., 2022; ExoJAX Kawahara & others, 2022; VLIDORT Spurr, 2006; DISORT Stamnes et al., 1988; Rodgers, inverse methods Rodgers, 2000.
PyMieDiff, arXiv 2512.08614; Ray-trax, arXiv 2511.09389; JAXopt’s README (no longer maintained).
plumax, gaussx, geotoolz, georeader, finitevolX, spectraldiffx, pyrox and somax at the revisions in the header.
- Johnson, J. E. (2026). A differentiable JAX/Equinox RTM for methane and multi-gas remote sensing: design and staged roadmap. Unpublished design note (as rtmx).
- Korkin, S., Sayer, A. M., Ibrahim, A., & Lyapustin, A. (2022). A practical guide to writing a radiative transfer code. Computer Physics Communications, 271, 108198.
- Kawahara, H., & others. (2022). Autodifferentiable spectrum model for high-dispersion characterization of exoplanets and brown dwarfs. The Astrophysical Journal Supplement Series, 258, 31.
- Spurr, R. (2006). VLIDORT: a linearized pseudo-spherical vector discrete ordinate radiative transfer code for forward model and retrieval studies in multilayer multiple scattering media. Journal of Quantitative Spectroscopy and Radiative Transfer, 102, 316–342.
- Stamnes, K., Tsay, S.-C., Wiscombe, W., & Jayaweera, K. (1988). Numerically stable algorithm for discrete-ordinate-method radiative transfer in multiple scattering and emitting layered media. Applied Optics, 27(12), 2502–2509.
- Rodgers, C. D. (2000). Inverse Methods for Atmospheric Sounding: Theory and Practice. World Scientific.