Before any dispersion model, you need three things that every tier shares: meteorological forcing (winds, turbulence, PBL), static surface fields (orography, roughness, land-use), and the observation-side glue (L1/L2 ingest, averaging kernels). These are not models — they are the data interfaces all tiers read from. Getting them right early pays off because all four tiers will read from the same APIs; getting them wrong forces parallel ports of every tier later.
The page is grouped into:
Forcing — meteorology (dynamic 3D fields)
Static surface fields (orography, land-use, roughness)
Geometry (coordinate frames and time)
Inversion priors (background emission inventory)
Observation side (L1/L2 ingest + averaging kernel)
The fixed forward interface is the contract that ties them together.
1 · Forcing — meteorology¶
Reanalysis-agnostic met reader¶
Met forcing has a many-to-one interface. WRF is the highest-fidelity option (you control resolution, microphysics, nesting) and is the industry baseline, but ERA5 / MERRA-2 / HRRR / GEOS-FP are all valid forcing sources. The MetField PyTree is the abstraction; the WRF reader is one concrete loader.
Sources (priority order):
WRF-ARW NetCDF (
wrfout_d0X_*.nc) — primary.ERA5 (Copernicus CDS) — fallback for global / climatological work.
HRRR / GEOS-FP — operational near-real-time.
Output: a
MetFieldPyTree with consistent units, time stamping, and grid metadata (schema in Fixed forward interface).Interpolation: bilinear in horizontal, log-pressure or geometric height vertical.
Caching: pre-resampled grids cached to disk (zarr) so notebooks don’t re-interpolate WRF on every run. Cache key is
(source, native_grid_hash, target_grid_hash, time_window).
Planetary boundary layer (PBL) height¶
PBL height is not optional — Tier I plume rise, Tier II trajectory reflection, and Tier IV column partitioning all depend on it. WRF emits it as a diagnostic (PBLH); ERA5 has blh. Carry it as a first-class field on MetField, not buried inside a derived helper.
Pasquill–Gifford stability classifier¶
Maps surface observations (wind speed, cloud cover, time-of-day, solar elevation) to PG stability class A–F. Used by Gaussian-tier , parameterizations.
Reference: [turner1970], with [briggs1973] updates.
Output: per-grid-cell stability class as int (0–5) or one-hot.
Status: partially implemented in
gauss_plume/dispersion.py.
Monin–Obukhov similarity¶
Surface-layer wind and turbulence profiles. Provides and as functions of downwind distance, friction velocity , Obukhov length , and surface roughness .
Reference: [monin1954], [stull1988] (ch. 9).
Used by: Tier I Gaussian functions; Tier II particle-trajectory turbulence; Tier III sub-grid eddy diffusivity .
2 · Static surface fields¶
These are time-invariant geophysical fields. Distinct enough from dynamic met to deserve their own loader, but they live alongside MetField (typically broadcast against the time axis).
Surface roughness and land-use¶
Source: MODIS IGBP land-use (500 m global) or WRF static (
LU_INDEX+ table lookup).Used by: MO similarity ( directly), Tier II reflection (different reflection coefficients over water vs. land), Tier IV viewing-geometry corrections.
Status: ☐ not started.
Orography / terrain¶
Source: SRTM 90 m or GMTED2010; resampled to analysis grid.
Used by: Tier I AERMOD-style ([cimorelli2005aermod]) plume rise corrections; Tier II trajectory reflection off topography; Tier III boundary conditions and pressure-coordinate transforms.
Status: ☐ not started — currently only mentioned in the Tier I AERMOD aside.
3 · Geometry¶
Coordinate transforms¶
lat/lon ↔ local Cartesian(UTM or local tangent plane). Usepyprojfor the heavy lifting; wrap in a JAX-compatibleLocalFramePyTree so frame metadata travels with the data.pressure ↔ geometric heightvia hydrostatic balance + WRF temperature profile.timenormalized to UTC throughout; never mix in local time downstream.
Time and calendar¶
UTC throughout — but enforce it. A small Timestamp PyTree (epoch_seconds: int64, tz: Literal["UTC"]) makes the invariant load-bearing instead of aspirational. Document leap-year / leap-second handling at the boundary (most projects botch this once and never again).
4 · Inversion priors¶
Background emission inventory ¶
Every Bayesian inversion (Tiers I–IV, Step 2) uses a prior over the source field. Naming a single source matters because results are sensitive to it.
Sources: EDGAR v8 ([crippa2023edgar], global anthropogenic, ~0.1°), GFEI ([scarpelli2022gfei], oil & gas, ~0.1°), gridded EPA GHGI ([maasakkers2023ghgi,epa_ghgi], US gridded), Scarpelli et al. ([scarpelli2020sectoral], sectoral CH₄).
Output: spatial source field on the analysis grid + sectoral metadata + uncertainty estimate (typically lognormal ).
Used by: all (background covariance) constructions in Tier II/III inversion; the prior on for Tier I MAP/MCMC.
Status: ☐ not started.
5 · Observation side¶
L1 / L2 ingest¶
Symmetric to the met reader. Parses raw satellite products into the shared Observations PyTree.
Inputs: TROPOMI ([s5p_tropomi], NetCDF), GHGSat ([ghgsat,jervis2021ghgsat], HDF5), EMIT ([emit], NetCDF), Tanager ([carbon_mapper], TIFF + sidecar JSON), Sentinel-2 ([sentinel2]) / Landsat ([landsat89], TIFF). One sub-loader per instrument, dispatched by file extension + product header.
Output:
ObservationsPyTree (radiance or column XCH₄ + lat/lon footprint + time + per-pixel uncertainty + quality mask + AK).Status: ☐ not started — the satellite catalog describes the targets; the ingest layer is the missing implementation.
Averaging-kernel operator¶
Applies the satellite averaging kernel to a model column:
where is the model state (CH₄ mixing-ratio profile), is the prior used in the L2 retrieval, is the column-averaging weighting, and is the satellite-product averaging kernel matrix. The construction follows the optimal-estimation framework of [rodgers2000]. Needed by Tiers II–IV when comparing to L2 XCH₄ products instead of L1 radiances.
Status: scaffold in
assimilation/obs_operator.py.Provider design: one
Instrumentregistry that returns keyed on instrument name. Single hook avoids per-tier branching.
Fixed forward interface¶
All four tiers implement the same shape:
def forward(params: Params, met: MetField) -> Observations:
"""Map source/state parameters + met forcing → simulated observations.
Each tier provides its own concrete `Params` PyTree, but the call
signature, return type, and JAX traceability are identical, so any
inference loop (NumPyro, vardaX, filterax) takes any tier as a drop-in.
"""MetField schema¶
The single most-shared object in plumax. Concrete fields, units, and conventions:
MetField PyTree fields, shapes, units, and provenance.
| Field | Shape | dtype | Units | Source | Notes |
|---|---|---|---|---|---|
u, v, w | (T, Z, Y, X) | f32 | m/s | dynamic | wind components, cell-centred |
T | (T, Z, Y, X) | f32 | K | dynamic | temperature |
p | (T, Z, Y, X) | f32 | Pa | dynamic | pressure |
q | (T, Z, Y, X) | f32 | kg/kg | dynamic | water vapour mixing ratio |
tke | (T, Z, Y, X) | f32 | m²/s² | dynamic | turbulent KE (optional; some loaders provide) |
pblh | (T, Y, X) | f32 | m | dynamic | PBL height |
z0 | (Y, X) | f32 | m | static | surface roughness |
lu | (Y, X) | i8 | — | static | land-use class (IGBP) |
hgt | (Y, X) | f32 | m | static | terrain elevation |
frame | — | static metadata | — | static | LocalFrame (pyproj-built, non-traced) |
time | (T,) | i64 | UTC seconds | static metadata | Timestamp PyTree |
ensemble_dim | scalar | int | — | static metadata | 0 for deterministic; >0 carries an outer ensemble axis on dynamic fields |
Conventions:
Coordinate axes ordered
(time, vertical, y, x)always — never reordered downstream.coordaxis the natural representation; the loader returns acoordax.Datasetwith these names.Units enforced at load time; downstream code may assume them.
Time alignment: dynamic fields are time-stamped at their native cadence (typically hourly). The forward interface accepts a temporal interpolation policy (snapshot / piecewise-linear / nearest) — see open questions.
Params and Observations¶
Params: tier-specific PyTree (e.g. for Tier I, field for Tier III).Observations: per-instrument PyTree from the L1/L2 ingest layer — radiances or column XCH₄ + footprint + mask + uncertainty + AK. Same shape as the L1/L2 product the satellite returned.
This contract is what makes Step 6 (“upgrade any component”) tractable: replace forward with an emulator, the inference loop doesn’t notice.
Module layout¶
Module-level breakdown — concern, target module, status, downstream blockers.
| Concern | Module | Status | Blocks |
|---|---|---|---|
| Met loader (WRF) | met/wrf.py (load_wrf) + met/field.py (MetField, to_prescribed_wind) | ✓ v1 — destagger, η→z per column, bilinear onto the C-grid stagger, piecewise-linear in time; origin places the grid until frames land (#79) | Tier II, III |
| Met loader (ERA5) | plumax.met.era5 | ☐ #77 | global Tier II/III |
| PBL diagnostics | plumax.met.pbl | ☐ #78 | Tier II reflection, Tier IV partitioning |
| Static fields (, LU, terrain) | plumax.met.static | ☐ #78 | MO similarity, Tier III BCs |
| PG stability | plumax.gauss_plume.dispersion | 🚧 partial | Tier I (only) |
| MO similarity | plumax.met.surface_layer | ☐ #78 | Tier II turbulence, Tier III diffusivity |
| Coord transforms | plumax.met.frames | ☐ #79 | all tiers |
| Time / Timestamp | plumax.met.time | ☐ #79 | all tiers |
| Emission inventory loader | plumax.priors.inventory | ☐ #81 | Tier I–IV inversion priors |
| L1/L2 ingest | plumax.obs.ingest | ☐ #80 | Tier IV (and any real-data work) |
| AK operator | plumax.assimilation.obs_operator (radiance model) + coupled.Instrument (AK payload) | 🚧 — shared column + AK pipeline #83; L2 ingest builds Instruments #80 | Tier IV column-space comparison |
plumax.met now exists (the MetField container and the WRF loader); the remaining met concerns land beside them. plumax.priors and plumax.obs are still proposed. All eleven concerns are tracked under the Tier 0 epic #69.
Validation strategy¶
Met reader: round-trip — read a WRF file, re-grid to the analysis grid, integrate column mass, compare to direct WRF column integration. Should agree to floating-point precision. CI fixture: pin a small synthetic
wrfout(~1 MB, 5×5×10×3) undertests/fixtures/met/so the test runs without external downloads.Reanalysis parity: load the same time window from WRF and ERA5, regrid both to a coarse common grid, compare column-mean wind speed. Should agree to within climatological variability — confirms the loaders share conventions.
PG classifier: reproduce the textbook table from [turner1970] for canonical (wind, cloud, hour) inputs.
MO similarity: cross-check against
metpy.calc.surface_layer_*for a handful of sounding inputs.Coordinate transforms: round-trip lat/lon → UTM → lat/lon, max error < 1 mm.
Inventory loader: integrate EDGAR ([crippa2023edgar]) over a known basin (Permian) and compare to the published basin total in the EDGAR documentation.
AK operator: apply identity AK and confirm (column average); apply published TROPOMI AK ([s5p_tropomi]) to a known profile and compare to the official L2 product.