Source population¶
Source population and forecasting: catalogues, size distributions, and point-process models. Tier V.
population
¶
Tier V — source population & forecasting.
Assembly of per-event Tier II-IV posteriors into population-scale products:
- :mod:
~plumax.population.catalog— the cross-tier posterior catalog (the load-bearing interface). - :mod:
~plumax.population.size_distribution— V.A instantaneous emission size distribution (hierarchical lognormal fit). - :mod:
~plumax.population.point_process— V.B spatio-temporal event-rate models (homogeneous Gamma-Poisson + log-linear inhomogeneous intensity). - :mod:
~plumax.population.intensity— V.B registry of 13 physically motivated source intensitiesλ(t)(equinox modules). - :mod:
~plumax.population.pod— V.B registry of 10 probability-of-detection modelsP_d(·)(equinox modules). "POD" here is probability of detection, not proper orthogonal decomposition. - :mod:
~plumax.population.paradox— V.D missing-mass paradox Monte Carlo. - :mod:
~plumax.population.fitting— POD-modified power-law NUTS fitter.
Importing this package is cheap: the NumPyro-dependent fit functions and
result types are bound lazily (PEP 562), so import plumax.population
never pulls in NumPyro.
EmissionCatalog
dataclass
¶
A population of detected emission events.
Attributes:
| Name | Type | Description |
|---|---|---|
events |
tuple[EmissionEvent, ...]
|
The detected events, one per (de-duplicated) detection. |
emission_rate
property
¶
Per-event posterior-mean emission rates (n_events,) [kg/s].
emission_std
property
¶
Per-event posterior standard deviations (n_events,) [kg/s].
from_events
classmethod
¶
Build a catalog from any sequence of events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events
|
Sequence[EmissionEvent]
|
The events to wrap (copied into a tuple). |
required |
Returns:
| Type | Description |
|---|---|
EmissionCatalog
|
The catalog. |
Source code in src/plumax/population/catalog.py
log_moments
¶
Per-event lognormal sufficient statistic (log_mean, log_std).
Each per-event posterior is summarised by a Gaussian mean m and
standard deviation s on Q. V.A needs the matching moments
in log-space, so we moment-match Q ~ Normal(m, s^2) to a
lognormal Q ~ LogNormal(log_mean, log_std^2) by equating the
first two moments:
.. math::
\log\_std^2 = \ln\!\left(1 + (s / m)^2\right), \qquad
\log\_mean = \ln(m) - \tfrac12 \log\_std^2.
This is the standard lognormal moment match. It is exact in the
small-:math:\sigma limit (coefficient of variation s / m
small) where the Gaussian and lognormal coincide, and degrades for
very poorly-constrained events (CV ≳ 1), which the V.A regime rule
flags for the full-sample path. Events with non-positive mean are
not representable in log-space and raise.
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
A tuple |
Source code in src/plumax/population/catalog.py
EmissionEvent
dataclass
¶
EmissionEvent(emission_rate: float, emission_std: float, x: float, y: float, time: float, tier: str, instrument: str | None = None)
A single detected emission event with its per-event posterior summary.
Attributes:
| Name | Type | Description |
|---|---|---|
emission_rate |
float
|
Posterior-mean emission rate |
emission_std |
float
|
Posterior standard deviation of |
x |
float
|
Source east-west location [m]. |
y |
float
|
Source north-south location [m]. |
time |
float
|
Detection / overpass time [s] (UTC seconds). |
tier |
str
|
Originating tier label, e.g. |
instrument |
str | None
|
Instrument identifier for per-satellite POD dispatch,
or |
PerEventPosterior
¶
Bases: Protocol
Structural type for a scalar per-event Q posterior summary.
The canonical scalar interface is a posterior mean and standard
deviation of the emission rate Q.
:class:plumax.coupled.fusion.FusionPosterior satisfies this directly;
the grid-vector Tier II posteriors are reduced to it by
:func:event_from_posterior.
SizeDistributionPosterior
dataclass
¶
Posterior over the lognormal population size-distribution parameters.
Attributes:
| Name | Type | Description |
|---|---|---|
mu_samples |
ndarray
|
Posterior samples of the log-scale location |
sigma_samples |
ndarray
|
Posterior samples of the log-scale spread |
summary
¶
Posterior mean / std / 95% credible interval for mu and sigma.
Returns:
| Type | Description |
|---|---|
dict[str, dict[str, float]]
|
A nested dict keyed by |
dict[str, dict[str, float]]
|
|
Source code in src/plumax/population/size_distribution.py
fitted_pdf
¶
Posterior-mean fitted lognormal density evaluated at q.
Evaluates the lognormal pdf at the posterior-mean (mu, sigma).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
q
|
ndarray
|
Emission rates [kg/s] at which to evaluate the density; must be strictly positive. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The density values, same shape as |
Source code in src/plumax/population/size_distribution.py
quantile
¶
Posterior-mean fitted-distribution quantile(s) of Q.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p
|
float | ndarray
|
Probability level(s) in |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The corresponding emission-rate quantile(s) [kg/s]. |
Source code in src/plumax/population/size_distribution.py
PoissonRatePosterior
dataclass
¶
Gamma posterior over a homogeneous Poisson rate lambda.
The conjugate update for n events observed over an exposure
area * duration with a Gamma(alpha0, beta0) prior on
lambda is Gamma(alpha0 + n, beta0 + area * duration).
Attributes:
| Name | Type | Description |
|---|---|---|
alpha |
float
|
Posterior Gamma shape. |
beta |
float
|
Posterior Gamma rate (inverse scale); has units of exposure. |
n_events |
int
|
Number of events observed. |
exposure |
float
|
The space-time exposure |
rate_mean
property
¶
Posterior-mean rate lambda [events per unit area per unit time].
credible_interval
¶
Equal-tailed posterior credible interval for lambda.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
level
|
float
|
Central probability mass, e.g. |
0.95
|
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
The |
Source code in src/plumax/population/point_process.py
InhomogeneousIntensityPosterior
dataclass
¶
Posterior over a log-linear inhomogeneous intensity model.
The model is log lambda_i = beta0 + covariates_i . beta evaluated at
the detection covariates, treated as an inhomogeneous-Poisson
log-intensity.
Attributes:
| Name | Type | Description |
|---|---|---|
beta0_samples |
ndarray
|
Posterior samples of the intercept |
beta_samples |
ndarray
|
Posterior samples of the coefficients
|
event_from_posterior
¶
event_from_posterior(posterior: PerEventPosterior, *, x: float, y: float, time: float, tier: str, instrument: str | None = None) -> EmissionEvent
Build an :class:EmissionEvent from any Tier II-IV posterior.
The posterior is duck-typed: a scalar (Q_mean, Q_std) is extracted
by :func:_scalar_q_summary, so the same adapter works for
:class:~plumax.coupled.fusion.FusionPosterior (read directly) and the
grid-vector :class:~plumax.lagrangian.inversion.GaussianPosterior /
:class:~plumax.lagrangian.inversion.LognormalPosterior (reduced to the
field total). For genuine multi-source overpasses, build one event per
source upstream.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
posterior
|
PerEventPosterior
|
A per-event posterior. |
required |
x
|
float
|
Source east-west location [m]. |
required |
y
|
float
|
Source north-south location [m]. |
required |
time
|
float
|
Detection / overpass time [s]. |
required |
tier
|
str
|
Originating tier label. |
required |
instrument
|
str | None
|
Instrument identifier, or |
None
|
Returns:
| Type | Description |
|---|---|
EmissionEvent
|
The corresponding :class: |
Source code in src/plumax/population/catalog.py
fit_lognormal_size_distribution
¶
fit_lognormal_size_distribution(catalog: EmissionCatalog, *, num_warmup: int = 500, num_samples: int = 500, num_chains: int = 1, mu_prior_scale: float = 5.0, sigma_prior_scale: float = 2.0, seed: int = 0) -> SizeDistributionPosterior
Hierarchical-Bayes fit of the lognormal population size distribution.
Fits mu and sigma of the population size distribution
Q ~ LogNormal(mu, sigma^2) from the per-event posteriors, with each
event's latent log Q_i marginalised analytically: because both the
population prior on log Q_i and the per-event soft observation are
Gaussian, the marginal likelihood of the observed log_mean_i is
.. math::
\text{log\_mean}_i \sim \text{Normal}(\mu, \sigma^2 + \text{log\_std}_i^2).
The per-event log_std_i widens the effective likelihood variance,
so poorly-constrained events propagate their uncertainty into the
(mu, sigma) posterior (a catalog of wide events yields a wider
theta posterior than naively fitting the point estimates).
NumPyro is imported lazily here so the module import stays NumPyro-free.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
EmissionCatalog
|
The per-event posterior catalog (≥ 1 event). |
required |
num_warmup
|
int
|
NUTS warmup iterations. |
500
|
num_samples
|
int
|
Posterior samples to draw (per chain). |
500
|
num_chains
|
int
|
Number of NUTS chains. |
1
|
mu_prior_scale
|
float
|
Std of the |
5.0
|
sigma_prior_scale
|
float
|
Scale of the |
2.0
|
seed
|
int
|
PRNG seed for the sampler. |
0
|
Returns:
| Type | Description |
|---|---|
SizeDistributionPosterior
|
The fitted :class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the catalog is empty. |
Source code in src/plumax/population/size_distribution.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
fit_poisson_rate
¶
fit_poisson_rate(catalog: EmissionCatalog, *, area: float, duration: float, prior_shape: float = 0.001, prior_rate: float = 0.001) -> PoissonRatePosterior
Closed-form Gamma-Poisson posterior for a homogeneous event rate.
Counts the events in catalog and forms the conjugate
Gamma(prior_shape + n, prior_rate + area * duration) posterior on
the rate lambda (events per unit area per unit time). No NumPyro
required.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
EmissionCatalog
|
The detected-event catalog. |
required |
area
|
float
|
Spatial exposure (area observed) [m^2 or chosen unit]. |
required |
duration
|
float
|
Temporal exposure (window length) [s or chosen unit]. |
required |
prior_shape
|
float
|
Gamma prior shape |
0.001
|
prior_rate
|
float
|
Gamma prior rate |
0.001
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
PoissonRatePosterior
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/plumax/population/point_process.py
fit_inhomogeneous_intensity
¶
fit_inhomogeneous_intensity(covariates: ndarray, *, quadrature_covariates: ndarray, quadrature_weights: ndarray, num_warmup: int = 500, num_samples: int = 500, num_chains: int = 1, beta_prior_scale: float = 5.0, seed: int = 0) -> InhomogeneousIntensityPosterior
Log-linear inhomogeneous Poisson-process intensity fit via NUTS.
Fits log lambda(x) = beta0 + x . beta as the intensity of an
inhomogeneous Poisson process. The point-process log-likelihood has the
standard two terms — the log-intensity summed over detected events minus
the integrated intensity over the observation domain
([daleyVereJones2008]):
log L(beta) = sum_i [beta0 + x_i . beta] - integral lambda(x) dx,
where the integral is approximated by quadrature over caller-supplied
background points,
integral lambda(x) dx ~= sum_q w_q * exp(beta0 + x_q . beta).
The exposure (integrated-intensity) term is what makes the rate
identifiable: without it the likelihood is maximised by lambda -> 1 at
every observed covariate and the fit cannot estimate an event rate over a
region / time window. The quadrature weights carry the domain measure
(area * duration), so the recovered beta0 is a true log-rate density.
NumPyro is imported lazily.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
covariates
|
ndarray
|
Detected-event covariate matrix |
required |
quadrature_covariates
|
ndarray
|
Background / integration covariate points
|
required |
quadrature_weights
|
ndarray
|
Per-point quadrature weights |
required |
num_warmup
|
int
|
NUTS warmup iterations. |
500
|
num_samples
|
int
|
Posterior samples (per chain). |
500
|
num_chains
|
int
|
Number of NUTS chains. |
1
|
beta_prior_scale
|
float
|
Std of the |
5.0
|
seed
|
int
|
PRNG seed for the sampler. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
InhomogeneousIntensityPosterior
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/plumax/population/point_process.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
The package also re-exports the intensity and probability-of-detection model classes, the missing-mass simulator, and (lazily) the NumPyro fitter. They are documented below, from the submodules that define them.
intensity — source intensities λ(t)¶
Tier V.B — methane source intensity functions as Equinox modules.
Physically motivated intensity functions λ(t) [events day⁻¹] for CH₄
emission sources observed by satellite remote sensing, packaged as
equinox.Module dataclasses. Each module bundles:
- deterministic parameters as frozen fields;
__call__(t)—λ(t)evaluation, compatible with jit / vmap / grad;sample_priors(cls)— a NumPyro prior factory returning a configured instance for MCMC inference.
Conventions:
- Time
tis always in days; fractional days encode sub-daily resolution. - Intensity
λ(t)is always in events day⁻¹. - All modules are immutable pytrees (equinox).
NumPyro is only imported inside sample_priors, so evaluating an
intensity never pulls it in.
INTENSITY_REGISTRY
module-attribute
¶
INTENSITY_REGISTRY = {'constant': ConstantIntensity, 'diurnal': DiurnalSinusoidalIntensity, 'seasonal': SeasonalSinusoidalIntensity, 'diurnal_seasonal': DiurnalSeasonalIntensity, 'modulated_diurnal': SeasonallyModulatedDiurnalIntensity, 'operational': OperationalScheduleIntensity, 'weibull_renewal': WeibullRenewalIntensity, 'periodic_batch': PeriodicBatchIntensity, 'coal_mine': CoalMineVentilationIntensity, 'landfill': LandfillIntensity, 'offshore': OffshorePlatformIntensity, 'wetland_permafrost': WetlandPermafrostIntensity, 'livestock_feedlot': LivestockFeedlotIntensity}
str → Module class mapping for dynamic dispatch and configuration files.
ConstantIntensity
¶
Bases: Module
Constant intensity for passive, continuously leaking infrastructure.
Physical Motivation
Abandoned oil/gas wells, corroded subsurface pipelines, orphaned wellheads. No external forcing — the infrastructure is under constant, unchanging stress producing uncoordinated micro-leaks at a flat background rate.
Typical sources: abandoned wells (~1–5 events day⁻¹), legacy distribution networks, chronic flange leaks.
Equation
λ(t) = λ₀ Λ(T) = λ₀ · T
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Constant emission rate [events day⁻¹]. Typical: 0.1–10.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(3.0)
Returns:
| Type | Description |
|---|---|
ConstantIntensity
|
A |
Source code in src/plumax/population/intensity.py
DiurnalSinusoidalIntensity
¶
Bases: Module
Diurnal sinusoidal intensity for thermally driven infrastructure.
Physical Motivation
Solar heating during afternoon hours (≈14:00) expands gas volumes inside storage tanks and pipeline networks, increasing PRV venting probability. Overnight cooling contracts the gas and suppresses emissions.
Dominant sub-daily pattern for geostationary platforms (MTG-IRS) or high-revisit LEO constellations (GHGSat daily, Tanager daily targeted).
Typical sources: tank farms, active compressor stations, LNG terminals, midstream gathering systems.
Equation
λ(t) = max(0, λ₀ + A · sin(2πt − φ)) φ = 2π · (φ_h / 24)
Period = 1 day. Over one full cycle: Λ(1) = λ₀.
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Baseline mean rate [events day⁻¹]. Must satisfy λ₀ > A. Typical: 2–8. |
amplitude |
float
|
Diurnal half-amplitude [events day⁻¹]. Typical: 1–4. |
phase_hours |
float
|
Hour-of-day of peak intensity [hours]. Typical: 14.0 (2 PM). |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(5.0) A ~ HalfNormal(3.0) φ_h ~ Normal(14.0, 2.0)
Source code in src/plumax/population/intensity.py
SeasonalSinusoidalIntensity
¶
Bases: Module
Annual sinusoidal intensity for biogenic / climate-driven sources.
Physical Motivation
Microbial methanogenesis in saturated soils peaks during warmest, wettest months. Dominates at regional/area-flux scales observed by wide-swath mappers: TROPOMI (7×5.5 km, daily), MethaneSAT (200 m, ~weekly).
Typical sources: tropical/boreal wetlands, rice paddies (monsoon Asia), permafrost thermokarst lakes, CAFOs.
Equation
λ(t) = max(0, λ₀ + A · sin(2π/365.25 · (t − t_peak)))
Over one full year: Λ(365.25) = λ₀ · 365.25.
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Annual mean rate [events day⁻¹]. Typical: 0.5–3.0. |
amplitude |
float
|
Seasonal half-amplitude [events day⁻¹]. Typical: 0.3–2.0. |
peak_day |
float
|
Day-of-year of emission peak [days from Jan 1]. Typical: 196 (Jul 15). |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(2.0) A ~ HalfNormal(1.5) t_peak ~ Normal(196.0, 30.0)
Source code in src/plumax/population/intensity.py
DiurnalSeasonalIntensity
¶
Bases: Module
Additive diurnal + seasonal intensity for thermally active biogenic sources.
Physical Motivation
Many sources exhibit both sub-daily thermal cycling and annual modulation. Landfills decompose faster on hot summer afternoons than cold winter nights. The aliasing between a satellite's fixed overpass time (Sentinel-2 at 10:30 AM, Landsat at 10:00 AM) and the diurnal peak systematically biases detection statistics.
Typical sources: municipal landfills, wastewater treatment plants, agricultural biodigesters, tropical rice paddies.
Equation
λ(t) = max(0, λ₀ + A_d·sin(2πt − φ_d) + A_s·sin(ω_s·(t − t_peak))) ω_s = 2π/365.25, φ_d = 2π·(φ_h/24)
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Grand mean intensity [events day⁻¹]. Typical: 1–5. |
amp_diurnal |
float
|
Diurnal half-amplitude [events day⁻¹]. |
amp_seasonal |
float
|
Seasonal half-amplitude [events day⁻¹]. |
phase_hours |
float
|
Hour of diurnal peak [hours]. Default: 14.0. |
peak_day |
float
|
Day of seasonal peak [days from Jan 1]. Default: 196.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(3.0) A_d ~ HalfNormal(2.0) A_s ~ HalfNormal(1.5) φ_h ~ Normal(14.0, 2.0) t_pk ~ Normal(196.0, 30.0)
Source code in src/plumax/population/intensity.py
SeasonallyModulatedDiurnalIntensity
¶
Bases: Module
Diurnal intensity whose amplitude is seasonally modulated.
Physical Motivation
Summer days produce much larger thermal swings (ΔT ≈ 20 K) than winter (ΔT ≈ 5 K). The diurnal emission peak is 3–4× larger in July than January. This multiplicative coupling is the dominant pattern at large landfills and open-pit coal mines observed from orbiting hyperspectral imagers (GHGSat 25 m, Tanager 30 m).
Equation
A_d(t) = (A_sum + A_win)/2 + (A_sum − A_win)/2 · sin(ω_s·(t − t_pk)) λ(t) = max(0, λ₀ + A_d(t) · sin(2πt − φ_d))
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Grand mean intensity [events day⁻¹]. |
amp_summer |
float
|
Diurnal half-amplitude at seasonal peak [events day⁻¹]. |
amp_winter |
float
|
Diurnal half-amplitude at seasonal trough [events day⁻¹]. |
phase_hours |
float
|
Hour of diurnal peak [hours]. Default: 14.0. |
peak_day |
float
|
Day of seasonal maximum [days from Jan 1]. Default: 196.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(4.0) A_sum ~ HalfNormal(3.0) A_win ~ HalfNormal(1.0) φ_h ~ Normal(14.0, 2.0) t_peak ~ Normal(196.0, 30.0)
Source code in src/plumax/population/intensity.py
OperationalScheduleIntensity
¶
Bases: Module
Step-function intensity for facilities with fixed shift schedules.
Physical Motivation
Compressor stations, pump-jacks, and mine ventilation fans operate on strict crew shifts. Machinery ON → pressure cycling and mechanical stress generate leaks at λ_active. Machinery OFF → passive background at λ_idle.
Critical for interpreting polar-orbiting satellites with fixed equatorial crossing times: Sentinel-2 (10:30), Landsat (10:00), TROPOMI (13:30). A satellite that always observes during the active window systematically overestimates the 24-hour average.
Equation
λ(t) = λ_idle + (λ_active − λ_idle) · [σ(h − h_start) − σ(h − h_end)] h = (t mod 1) · 24, σ(x) = 1/(1 + e^(−50x))
Uses smooth sigmoid transitions (k=50) for differentiability in NUTS.
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_active |
float
|
Intensity during operational hours [events day⁻¹]. Typical: 3–15. |
lambda_idle |
float
|
Intensity during shutdown [events day⁻¹]. Typical: 0.1–1.0. |
start_hour |
float
|
Start of active window [hours 0–24]. Default: 6.0. |
end_hour |
float
|
End of active window [hours 0–24]. Default: 18.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ_active ~ HalfNormal(8.0) λ_idle ~ HalfNormal(1.0) h_start ~ Normal(6.0, 1.0) h_end ~ Normal(18.0, 1.0)
Source code in src/plumax/population/intensity.py
WeibullRenewalIntensity
¶
Bases: Module
Weibull hazard function for pressure-driven, recharging sources.
Physical Motivation
PRVs on LNG tanks and high-pressure wellheads don't fail randomly. After venting, pressure drops to zero and must physically recharge. The hazard starts at zero and climbs — a textbook increasing failure rate (IFR).
This is a Renewal Process: intensity depends on elapsed time since the most recent event (Δt), not calendar time. Generalises the memoryless Exponential (k=1) to IFR (k>1) or DFR (k<1).
Observable from any high-revisit platform (GHGSat, Tanager, geostationary).
Equation
h(Δt) = (k/η) · (Δt/η)^(k−1)
k > 1 → hazard increases (pressure recharge).
k = 1 → constant hazard (memoryless Exponential).
Attributes:
| Name | Type | Description |
|---|---|---|
scale |
float
|
Weibull scale η [days]. Typical: 0.05–2.0. |
shape |
float
|
Weibull shape k [dimensionless]. Typical: 2.0–5.0. |
Note
__call__ takes t_since_last (Δt), not absolute time.
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
η ~ HalfNormal(0.5) [days] k ~ LogNormal(1.0, 0.5) [dimensionless]
Source code in src/plumax/population/intensity.py
PeriodicBatchIntensity
¶
Bases: Module
Periodic narrow-pulse intensity for scheduled batch venting.
Physical Motivation
Glycol dehydrators flash accumulated CH₄ every 4–8 hours. Tank batteries undergo liquid unloading every 12–48 hours producing sharp spikes lasting minutes. Coal mine longwall shearers expose fresh seam every 6–12 hours.
Between batches, only minor fugitive leaks persist. The result is a comb-like Gaussian pulse train on a low background.
Observable primarily by high-revisit tasked satellites (GHGSat, Tanager) or continuous ground-based sensors.
Equation
λ(t) = λ_bg + (λ_pk − λ_bg) · exp(−((t mod P) − P/2)² / (2·(f·P)²))
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_background |
float
|
Inter-batch fugitive rate [events day⁻¹]. Typical: 0.1–1.0. |
lambda_peak |
float
|
Peak rate during a batch vent [events day⁻¹]. Typical: 10–50. |
period_days |
float
|
Time between successive batches [days]. Typical: 0.2–2.0. |
duty_fraction |
float
|
Pulse width as fraction of period [dimensionless]. Default: 0.05. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ_bg ~ HalfNormal(1.0) λ_pk ~ HalfNormal(20.0) P ~ LogNormal(ln(0.5), 0.5) [days] f ~ Beta(2, 20) [dimensionless]
Source code in src/plumax/population/intensity.py
CoalMineVentilationIntensity
¶
Bases: Module
Multi-scale operational intensity for underground coal mines.
Physical Motivation
Underground coal mines (>40 Mt CH₄ yr⁻¹ globally) exhibit three regimes: 1. Extraction shifts (06–22, Mon–Sat): longwall shearer advance exposes fresh seam, releasing adsorbed CH₄. Fans at full capacity. 2. Night idle (22–06): reduced crew, fans at minimum, slower desorption. 3. Weekly maintenance (Sunday): extraction halted, CH₄ builds in panels.
Observable by TROPOMI (daily, 7 km — integrates mine complex), MethaneSAT (weekly, 200 m — resolves vent shafts), GHGSat/Tanager (shaft-level).
Equation
maintenance_gate = exp(−(d_week − d_maint)² / (2·0.3²)) shift_gate = σ(h − h_start) − σ(h − h_end) λ(t) = gate_m · λ_maint + (1 − gate_m) · [gate_s · λ_ext + (1 − gate_s) · λ_idle]
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_extraction |
float
|
Active extraction rate [events day⁻¹]. Typical: 5–20. |
lambda_maintenance |
float
|
Maintenance shutdown rate [events day⁻¹]. Typical: 0.5–2. |
lambda_idle |
float
|
Nightly idle rate [events day⁻¹]. Typical: 1–4. |
shift_start |
float
|
Shift start [hours]. Default: 6.0. |
shift_end |
float
|
Shift end [hours]. Default: 22.0. |
maintenance_day |
float
|
Day of 7-day week for maintenance [0=Mon, 6=Sun]. Default: 6.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ_ext ~ HalfNormal(10.0) λ_maint ~ HalfNormal(1.5) λ_idle ~ HalfNormal(3.0)
Source code in src/plumax/population/intensity.py
LandfillIntensity
¶
Bases: Module
Multi-component intensity for municipal solid waste landfills.
Physical Motivation
Three superimposed physical mechanisms: 1. Seasonal temperature (annual): soil T governs microbial activity. Summer peak → 2–3× winter rates. 2. Diurnal thermal (daily): solar heating warms cover soil, enhancing gas diffusion. Peak ≈ 15:00 local. 3. Barometric pumping (synoptic, ~3–7 days): passing low-pressure systems literally suck CH₄ out through the cover soil.
Observable by all satellite tiers: TROPOMI (regional), MethaneSAT (complex-scale), GHGSat/Tanager (cell-level).
Equation
λ(t) = max(0, λ₀ + A_s·sin(ω_s(t−t_pk)) + A_d·sin(ω_d·t−φ_d) + A_b·sin(ω_b·t)) ω_s=2π/365.25, ω_d=2π, ω_b=2π/P_baro
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Annual mean [events day⁻¹]. Typical: 2–6. |
amp_seasonal |
float
|
Seasonal half-amplitude [events day⁻¹]. Typical: 1–3. |
amp_diurnal |
float
|
Diurnal half-amplitude [events day⁻¹]. Typical: 0.5–2. |
amp_barometric |
float
|
Barometric pumping half-amplitude [events day⁻¹]. Typical: 0.5–1.5. |
baro_period_days |
float
|
Synoptic weather period [days]. Default: 5.0. |
peak_day |
float
|
Seasonal peak [days from Jan 1]. Default: 196.0. |
phase_hours |
float
|
Diurnal peak hour [hours]. Default: 15.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(4.0) A_s ~ HalfNormal(2.0) A_d ~ HalfNormal(1.5) A_b ~ HalfNormal(1.0) P_baro ~ LogNormal(ln5, 0.3) t_peak ~ Normal(196, 20) φ_h ~ Normal(15, 1.5)
Source code in src/plumax/population/intensity.py
OffshorePlatformIntensity
¶
Bases: Module
Combined tidal and operational intensity for offshore oil & gas platforms.
Physical Motivation
Semi-diurnal tides (M2: ~12.42 hr) modulate subsurface hydrostatic pressure, altering gas migration through sediments. Crew shift schedules simultaneously drive compressor cycling and flaring. These two independent forcings superimpose.
Observable from Sentinel-2 (2–5 day, flare detection) and GHGSat/Tanager (hyperspectral, fugitive plume quantification).
Equation
λ(t) = max(0, λ₀ + A_tide·sin(2π/P_tide · t) + A_ops·[σ(h−h_on)−σ(h−h_off)])
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Baseline platform rate [events day⁻¹]. Typical: 3–8. |
amp_tidal |
float
|
Tidal half-amplitude [events day⁻¹]. Typical: 0.5–2. |
amp_operational |
float
|
Crew-shift half-amplitude [events day⁻¹]. Typical: 1–4. |
tidal_period_days |
float
|
Dominant tidal period [days]. Default: 0.5175 (M2 ≈ 12.42 hr). |
shift_start |
float
|
Day crew start [hours]. Default: 7.0. |
shift_end |
float
|
Day crew end [hours]. Default: 19.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(5.0) A_tide ~ HalfNormal(1.5) A_ops ~ HalfNormal(2.0)
Source code in src/plumax/population/intensity.py
WetlandPermafrostIntensity
¶
Bases: Module
Sigmoid-gated seasonal intensity for high-latitude wetland/permafrost.
Physical Motivation
Arctic/boreal wetlands (~200 Mt CH₄ yr⁻¹) are gated by a binary physical constraint: soil must be thawed and saturated for methanogenesis. When the active layer freezes (Oct–Apr), emissions collapse to near zero. Thaw onset (May) ramps emissions over ~2–4 weeks. This sharp on/off is fundamentally different from a smooth sinusoid.
Observable by TROPOMI (daily, 7 km — diffuse wetland complexes), MethaneSAT (weekly, 200 m — thermokarst lake clusters), GOSAT (~3 day).
Equation
G_thaw(t) = σ((t_yr − t_thaw) / w) G_freeze(t) = 1 − σ((t_yr − t_freeze) / w) λ(t) = λ_frozen + (λ_peak − λ_frozen) · G_thaw · G_freeze t_yr = t mod 365.25
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_peak |
float
|
Peak summer rate [events day⁻¹]. Typical: 2–8. |
lambda_frozen |
float
|
Winter baseline (residual ebullition) [events day⁻¹]. Typical: 0.01–0.3. |
thaw_onset_day |
float
|
Day-of-year of thaw onset [days]. Default: 120 (≈ May 1). |
freeze_onset_day |
float
|
Day-of-year of freeze onset [days]. Default: 280 (≈ Oct 7). |
transition_width |
float
|
Sigmoid transition width [days]. Default: 15.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ_peak ~ HalfNormal(5.0) λ_frozen ~ HalfNormal(0.2) t_thaw ~ Normal(120, 15) t_freeze ~ Normal(280, 15) w ~ HalfNormal(15.0)
Source code in src/plumax/population/intensity.py
LivestockFeedlotIntensity
¶
Bases: Module
Bimodal diurnal + seasonal intensity for concentrated livestock operations.
Physical Motivation
Enteric fermentation in cattle (~100–120 kg CH₄ cow⁻¹ yr⁻¹) peaks 1–3 hours after feeding as rumen microbial activity surges. Large CAFOs have fixed AM/PM feeding schedules → characteristic bimodal diurnal pattern. Seasonal modulation from temperature-dependent manure lagoon emissions and feed composition changes.
Observable at facility scale by GHGSat (25 m), Tanager (30 m), and regionally by TROPOMI and MethaneSAT.
Equation
G_am(t) = exp(−(h − h_am)² / (2w²)) G_pm(t) = exp(−(h − h_pm)² / (2w²)) λ(t) = max(0, λ₀ + A_f·(G_am + G_pm) + A_s·sin(ω_s(t − t_peak))) h = (t mod 1)·24
Attributes:
| Name | Type | Description |
|---|---|---|
lambda_0 |
float
|
Baseline enteric rate [events day⁻¹]. Typical: 1–4. |
amp_feeding |
float
|
Post-feeding surge amplitude [events day⁻¹]. Typical: 1–3. |
amp_seasonal |
float
|
Seasonal half-amplitude [events day⁻¹]. Typical: 0.5–1.5. |
morning_feed_hour |
float
|
AM feed time [hours]. Default: 7.0. |
evening_feed_hour |
float
|
PM feed time [hours]. Default: 17.0. |
feed_response_width |
float
|
Gaussian width of feeding pulse [hours]. Default: 2.0. |
peak_day |
float
|
Seasonal peak [days from Jan 1]. Default: 196.0. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
λ₀ ~ HalfNormal(3.0) A_f ~ HalfNormal(2.0) A_s ~ HalfNormal(1.0) h_am ~ Normal(7.0, 1.0) h_pm ~ Normal(17.0, 1.0) w ~ HalfNormal(2.0)
Source code in src/plumax/population/intensity.py
pod — probability of detection P_d(·)¶
"POD" here is probability of detection, not proper orthogonal decomposition.
Tier V.B — probability-of-detection (POD) models as Equinox modules.
Here "POD" means probability of detection — the chance that a satellite detects a methane plume of a given size under given conditions. (It is unrelated to the proper orthogonal decomposition used for reduced-order modelling elsewhere in the docs.)
A family of GLM-style POD models P_d(x) ∈ [0, 1] for methane plume
detectability from satellite remote sensing, packaged as eqx.Module.
Each module bundles:
- covariate transformation + link function in
__call__; sample_priors(cls)— a NumPyro prior factory for MCMC.
The progression mirrors :mod:plumax.population.intensity: simple →
complex, each model relaxing an assumption of the last.
Detection is not a deterministic threshold. It is a probabilistic event
governed by the interplay of source strength, atmospheric dilution,
instrument optics and radiometric conditions, modelled as a Bernoulli GLM
Y ~ Bernoulli(P_d(x)) where Y ∈ {0, 1} (detected / not detected)
and P_d is evaluated at covariates x.
Conventions:
- Flux
Qin [kg hr⁻¹]; wind speedUin [m s⁻¹]; pixel sizepin [m]; albedoAin [–],∈ [0, 1]. - Solar zenith angle
θin [°]; models usecos(θ) ∈ [0.26, 1.0]. - Spectral bands
n_bands[dimensionless count]; noiseσ_noisein [ppb] or instrument-specific units. - All modules are immutable pytrees, jit / vmap / grad compatible.
NumPyro is only imported inside sample_priors, so evaluating a POD
curve never pulls it in.
POD_REGISTRY
module-attribute
¶
POD_REGISTRY = {'logistic': LogisticPOD, 'log_logistic': LogLogisticPOD, 'concentration_proxy': ConcentrationProxyPOD, 'additive_multi': AdditiveMultiCovariatePOD, 'varying_coefficient': VaryingCoefficientPOD, 'snr_based': SNRBasedPOD, 'spectral_aware': SpectralAwarePOD, 'full_varying': FullVaryingCoefficientPOD, 'probit': ProbitPOD, 'cloglog': CloglogPOD}
str → POD Module class mapping for dynamic dispatch.
LogisticPOD
¶
Bases: Module
Single-covariate logistic POD: the classic S-curve.
Physical Motivation
The simplest detection model: probability depends only on flux Q through a logistic sigmoid. This is the "Q₅₀" model universally used in first-pass satellite characterisation. The satellite has a detection midpoint Q₅₀ where P_d = 0.5, and a steepness k controlling how sharply the curve transitions from 0 → 1.
Ignores wind, albedo, zenith — appropriate for controlled-release experiments under constant atmospheric conditions or as a baseline.
Equation
P_d(Q) = σ(k · (Q − Q₅₀)) = 1 / (1 + exp(−k · (Q − Q₅₀)))
Equivalent logit form:
logit(P_d) = k · (Q − Q₅₀) = k·Q − k·Q₅₀ = β₁·Q + β₀
Attributes:
| Name | Type | Description |
|---|---|---|
Q_50 |
float
|
Flux at 50% detection probability [kg hr⁻¹]. Typical: GHGSat ≈ 100, TROPOMI ≈ 3000, MethaneSAT ≈ 200. |
k |
float
|
Steepness of the sigmoid [hr kg⁻¹]. Typical: 0.005–0.05. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
Q₅₀ ~ LogNormal(ln(500), 1.0) [kg hr⁻¹] k ~ HalfNormal(0.02) [hr kg⁻¹]
Source code in src/plumax/population/pod.py
LogLogisticPOD
¶
Bases: Module
Logistic POD on log-flux: the lognormal CDF approximation.
Physical Motivation
Because flux spans orders of magnitude, the logistic curve on raw Q is poorly calibrated: it transitions too slowly across the super- emitter tail. Operating in log-space produces the lognormal CDF shape used by IMEO/Kamdar et al. for per-satellite POD fits.
This is the model behind james_jax.py: P_d(Q) ≈ Φ_LN(Q; Q₅₀, σ).
Equation
logit(P_d) = β₀ + β₁ · ln(Q)
Equivalently, the transition midpoint is at Q = exp(−β₀/β₁).
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [logit units]. Controls the detection threshold. |
beta_1 |
float
|
Slope w.r.t. ln(Q) [logit per ln(kg hr⁻¹)]. Positive → higher flux → more detectable. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−8.0, 3.0) [logit] β₁ ~ HalfNormal(2.0) [logit per ln(kg hr⁻¹)]
Source code in src/plumax/population/pod.py
ConcentrationProxyPOD
¶
Bases: Module
GLM on the log-concentration proxy ln(Q/(U·p)).
Physical Motivation
The column-integrated methane enhancement ΔΩ ∝ Q/(U·p) is the primary observable. This model captures the three dominant controls in a single log-linear predictor. Assumes their effects are multiplicatively separable in concentration space (additive in log-space).
Used in Varon et al. (2018), the canonical IME framework.
Equation
logit(P_d) = β₀ + β₁ · ln(Q / (U · p))
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [logit units]. |
beta_1 |
float
|
Sensitivity to log-concentration proxy [logit per dimensionless unit]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−3.0, 2.0) [logit] β₁ ~ HalfNormal(2.0) [logit per unit]
Source code in src/plumax/population/pod.py
AdditiveMultiCovariatePOD
¶
Bases: Module
Additive GLM with separate coefficients for each covariate.
Physical Motivation
Relaxes the single-proxy assumption: flux, wind, pixel size, albedo, and illumination each receive an independent coefficient. This allows the model to learn that, e.g., wind has a stronger dilution effect than pixel size, or that albedo matters more than zenith angle.
The linear predictor is a standard multivariate logistic regression.
Equation
logit(P_d) = β₀ + β_Q·ln(Q) + β_U·ln(U) + β_p·ln(p) + β_A·A + β_θ·cos(θ)
Expected sign constraints (encoded in priors):
• β_Q > 0 (more flux → more detectable)
• β_U < 0 (more wind → more dilution → less detectable)
• β_p < 0 (larger pixel → more fill-factor dilution)
• β_A > 0 (brighter surface → better SNR)
• β_θ > 0 (higher sun → less atmospheric path → cleaner)
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [logit]. |
beta_Q |
float
|
Log-flux coefficient [logit per ln(kg hr⁻¹)]. |
beta_U |
float
|
Log-wind coefficient [logit per ln(m s⁻¹)]. |
beta_p |
float
|
Log-pixel coefficient [logit per ln(m)]. |
beta_A |
float
|
Albedo coefficient [logit per unit albedo]. |
beta_theta |
float
|
Cosine-zenith coefficient [logit per unit cos(θ)]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors (sign-constrained where physics dictates): β₀ ~ Normal(−5.0, 3.0) β_Q ~ HalfNormal(2.0) (positive: more flux → detected) β_U ~ Normal(−1.0, 1.0) (negative: dilution) β_p ~ Normal(−0.5, 0.5) (negative: fill-factor) β_A ~ HalfNormal(1.5) (positive: brighter → better) β_θ ~ HalfNormal(1.0) (positive: higher sun → better)
Source code in src/plumax/population/pod.py
VaryingCoefficientPOD
¶
Bases: Module
Varying coefficient GLM: sensitivity β₁ depends on environment.
Physical Motivation
The slope of the detection curve is NOT constant — it changes with observing conditions. Under high albedo and overhead sun, the instrument's spectral retrieval is cleaner, so the sigmoid is steeper (the satellite becomes a better detector). Under low albedo or high zenith, the slope flattens.
This is the "Refined Varying Coefficient Formulation" from the pod_numpyro.py module:
β₁(Z) = γ_base + γ_A · A + γ_θ · cos(θ)
The key insight: albedo and zenith don't shift the curve left/right (that's the additive model) — they change how sharply the instrument discriminates between detectable and undetectable plumes.
Equation
β₁(Z) = γ_base + γ_A · A + γ_θ · cos(θ) logit(P_d) = β₀ + β₁(Z) · ln(Q / (U · p))
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Baseline threshold [logit]. Typical: [−6, −2]. |
gamma_base |
float
|
Intrinsic instrument sensitivity [logit per log-proxy-unit]. Typical: [0.5, 3.0]. |
gamma_albedo |
float
|
Albedo enhancement [logit per log-unit per unit albedo]. Typical: [0.5, 2.0]. |
gamma_cos_theta |
float
|
Illumination enhancement [logit per log-unit per unit cos(θ)]. Typical: [0.3, 1.5]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−3.5, 2.0) γ_b ~ HalfNormal(1.5) γ_A ~ HalfNormal(1.0) γ_θ ~ Normal(0.5, 0.5)
Source code in src/plumax/population/pod.py
SNRBasedPOD
¶
Bases: Module
GLM on log-SNR: radiometric first-principles detection model.
Physical Motivation
At the hardware level, detection reduces to a hypothesis test on the retrieved methane column: "is the enhancement above the noise floor?" The test statistic is the signal-to-noise ratio:
SNR = ΔΩ / σ_retrieval ∝ (Q / (U·p)) · √A / σ_noise
where σ_noise encapsulates dark current, read noise, spectral fitting residuals, and atmospheric interference. Detection occurs when SNR > τ, which in the probabilistic GLM becomes:
P_d = σ(β₀ + β₁ · ln(SNR))
This model is appropriate when instrument noise characteristics are known or co-estimated, e.g., from calibration campaigns.
Equation
SNR = (Q / (U · p)) · √A / σ_noise logit(P_d) = β₀ + β₁ · ln(SNR)
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [logit]. Encodes the detection threshold in SNR space. |
beta_1 |
float
|
SNR sensitivity [logit per ln(SNR)]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−2.0, 2.0) [logit] β₁ ~ HalfNormal(2.0) [logit per ln(SNR)]
Source code in src/plumax/population/pod.py
SpectralAwarePOD
¶
Bases: Module
GLM with spectral band count and resolution as covariates.
Physical Motivation
Hyperspectral imagers (GHGSat: ~200 bands in SWIR, PRISMA: 250, EnMAP: 230) resolve individual CH₄ absorption lines, enabling matched-filter retrievals with far lower detection limits than multispectral instruments (Sentinel-2: ~13 bands, Landsat: ~11). The number of spectral channels in the CH₄-sensitive window (1600–1700 nm or 2300–2400 nm) directly governs retrieval precision.
Spectral resolution Δλ [nm] determines how well absorption lines are resolved. Finer resolution → better spectral discrimination → lower false alarm rate.
Equation
logit(P_d) = β₀ + β₁ · ln(Q/(U·p)) + β_n · ln(n_bands) + β_Δλ · ln(Δλ) + β_A · A
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [logit]. |
beta_proxy |
float
|
Concentration proxy sensitivity [logit per unit]. |
beta_n_bands |
float
|
Spectral channel count effect [logit per ln(count)]. |
beta_spectral_res |
float
|
Spectral resolution effect [logit per ln(nm)]. |
beta_albedo |
float
|
Albedo effect [logit per unit albedo]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−5.0, 3.0) β_x ~ HalfNormal(2.0) β_n ~ HalfNormal(1.0) (more bands → better) β_Δλ ~ Normal(−0.5, 0.5) (finer resolution → better, negative) β_A ~ HalfNormal(1.0)
Source code in src/plumax/population/pod.py
FullVaryingCoefficientPOD
¶
Bases: Module
Full varying-coefficient GLM with spectral and radiometric controls.
Physical Motivation
The most expressive model in the family. Both the intercept and the slope of the detection curve vary with environmental and instrumental state. This captures:
• Intercept modifiers (β₀ terms): direct additive effects on the logit baseline — e.g., more spectral bands directly lower the false-alarm threshold regardless of source strength. • Slope modifiers (γ terms): environment-dependent sensitivity — e.g., under high albedo, the instrument extracts more signal per unit concentration, steepening the POD curve.
Equation
β₁(Z) = γ_base + γ_A · A + γ_θ · cos(θ) + γ_n · ln(n_bands) β₀(Z) = β₀_base + β₀_noise · ln(σ_noise) logit(P_d) = β₀(Z) + β₁(Z) · ln(Q / (U · p))
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0_base |
float
|
Baseline intercept [logit]. |
beta_0_noise |
float
|
Noise intercept modifier [logit per ln(noise unit)]. |
gamma_base |
float
|
Intrinsic slope [logit per log-proxy-unit]. |
gamma_albedo |
float
|
Albedo slope modifier [logit per log-unit per albedo]. |
gamma_cos_theta |
float
|
Illumination slope modifier [logit per log-unit per cos(θ)]. |
gamma_n_bands |
float
|
Spectral slope modifier [logit per log-unit per ln(bands)]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀_base ~ Normal(−3.5, 2.0) β₀_noise ~ Normal(−0.5, 0.5) (more noise → lower baseline) γ_base ~ HalfNormal(1.5) γ_A ~ HalfNormal(1.0) γ_θ ~ Normal(0.5, 0.5) γ_n ~ HalfNormal(0.5)
Source code in src/plumax/population/pod.py
ProbitPOD
¶
Bases: Module
Probit GLM on the concentration proxy.
Physical Motivation
The probit link Φ(·) (standard normal CDF) arises naturally when the latent signal strength Y = β₀ + β₁·x + ε, with ε ~ N(0,1). A detection occurs when Y > 0. This is the correct model if the retrieval noise is Gaussian — which is a reasonable first approximation for well-calibrated optimal-estimation retrievals (e.g., IMAP-DOAS on TROPOMI, or the CO₂M matched filter).
The probit and logit links produce nearly identical curves; the probit has slightly thinner tails, producing a sharper transition.
Equation
P_d = Φ(β₀ + β₁ · ln(Q / (U · p)))
where Φ is the standard normal CDF.
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [probit units]. |
beta_1 |
float
|
Slope [probit per log-proxy unit]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−2.0, 2.0) [probit] β₁ ~ HalfNormal(1.5) [probit per unit]
Source code in src/plumax/population/pod.py
CloglogPOD
¶
Bases: Module
Complementary log-log GLM for asymmetric detection tails.
Physical Motivation
The logistic and probit links are symmetric: the curve approaches 0 and 1 at the same rate. In practice, P_d often approaches 1 much faster than it departs from 0 — once a plume is large enough, detection is near-certain, but very small plumes can linger near P_d ≈ 0 for a wide range of Q. The cloglog link captures this asymmetry naturally.
Arises from an extreme-value / Gumbel latent variable, appropriate when detection is a "first exceedance" event: the retrieval algorithm fires when the enhancement exceeds a noise threshold in at least one pixel.
Equation
P_d = 1 − exp(−exp(β₀ + β₁ · ln(Q / (U · p))))
Attributes:
| Name | Type | Description |
|---|---|---|
beta_0 |
float
|
Intercept [cloglog units]. |
beta_1 |
float
|
Slope [cloglog per log-proxy unit]. |
sample_priors
classmethod
¶
Sample NumPyro priors.
Priors
β₀ ~ Normal(−3.0, 2.0) [cloglog] β₁ ~ HalfNormal(1.5) [cloglog per unit]
Source code in src/plumax/population/pod.py
log_flux
¶
Log-transform of methane mass flux.
Physical Motivation
Flux spans 4+ orders of magnitude (1–10 000 kg hr⁻¹). The log compresses the super-emitter tail and linearises the relationship with detection probability in logit space.
Equation
x = ln(Q)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Q
|
ndarray
|
Methane mass flux [kg hr⁻¹]. Must be > 0. Shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Log-flux [ln(kg hr⁻¹)]. Shape |
Source code in src/plumax/population/pod.py
log_concentration_proxy
¶
Log concentration proxy combining flux, wind, and pixel size.
Physical Motivation
What a satellite actually "sees" is the column-integrated methane enhancement ΔΩ ∝ Q / (U · p). This proxy captures the three dominant first-order controls on detectability in a single dimensionless scalar: • Q ↑ → more CH₄ in the pixel → easier to detect. • U ↑ → plume diluted faster → harder to detect. • p ↑ → more background in the pixel → fill-factor dilution.
Used by Varon et al. (2018), Cusworth et al. (2021).
Equation
x = ln(Q / (U · p)) = ln(Q) − ln(U) − ln(p)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Q
|
ndarray
|
Methane mass flux [kg hr⁻¹]. Shape |
required |
U
|
ndarray
|
Wind speed [m s⁻¹]. Shape |
required |
p
|
ndarray
|
Pixel ground sampling distance [m]. Shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Log concentration proxy [dimensionless]. Shape |
Source code in src/plumax/population/pod.py
log_flux_wind_ratio
¶
Log flux-to-wind ratio (pixel-free proxy).
Physical Motivation
When pixel size is fixed for a single instrument, the Q/U ratio alone governs the column enhancement. This is the proxy used by the IMEO james_jax.py POD fits (Q/U in [kg hr⁻¹ / (m s⁻¹)]).
Equation
x = ln(Q / U)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Q
|
ndarray
|
Methane mass flux [kg hr⁻¹]. Shape |
required |
U
|
ndarray
|
Wind speed [m s⁻¹]. Shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Log flux-wind ratio [ln(kg hr⁻¹ / (m s⁻¹))]. Shape |
Source code in src/plumax/population/pod.py
snr_proxy
¶
Signal-to-noise ratio proxy for radiometric detection.
Physical Motivation
Detection fundamentally reduces to SNR. The signal is the column enhancement ΔΩ ∝ Q/(U·p). The noise floor depends on: • Albedo A: brighter surfaces → more reflected photons → lower shot noise → better SNR. • Instrument noise σ_n: dark current, read noise, spectral fitting residuals.
Equation
SNR = (Q / (U · p)) · √A / σ_noise
The model consumes ln(SNR) to linearise the logit relationship.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Q
|
ndarray
|
Methane mass flux [kg hr⁻¹]. Shape |
required |
U
|
ndarray
|
Wind speed [m s⁻¹]. Shape |
required |
p
|
ndarray
|
Pixel size [m]. Shape |
required |
albedo
|
ndarray
|
Surface albedo [–]. Shape |
required |
sigma_noise
|
ndarray
|
Instrument noise floor [ppb or instrument units]. Shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
ln(SNR) [dimensionless]. Shape |
Source code in src/plumax/population/pod.py
cos_zenith
¶
Cosine of solar zenith angle.
Physical Motivation
Solar zenith θ enters the radiative transfer equation through the air-mass factor 1/cos(θ). Higher sun (smaller θ, larger cos θ) means shorter path length → less atmospheric scattering → cleaner retrieval. Most instruments reject observations at θ > 70°.
Equation
x = cos(θ · π / 180)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta_deg
|
ndarray
|
Solar zenith angle [degrees]. Shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
cos(θ) [dimensionless], ∈ [0.26, 1.0] for θ ∈ [0°, 75°]. Shape |
Source code in src/plumax/population/pod.py
paradox — missing-mass Monte Carlo¶
Tier V.D — missing-mass paradox Monte Carlo (no plotting, no I/O).
A pedagogical numerical proof of the satellite "missing mass paradox": a satellite alerting system simultaneously overestimates the average emission rate of a facility while strictly underestimating the total emitted mass.
The paradox emerges from the thinned marked temporal point process (TMTPP) framework: a size-dependent probability of detection preferentially destroys small events, biasing sample means upward and sample sums downward.
The simulator has three strictly separated layers:
- Event generator — a temporal point process
λ(t)[events day⁻¹]. - Mark generator — a lognormal mark distribution
f(Q)overQ[kg hr⁻¹]. - Atmospheric filter — a parametric logistic POD
P_d(Q).
Units: t in days, λ in events day⁻¹, Q in kg hr⁻¹, D in
hr event⁻¹, M in kg.
Pure NumPy / SciPy (CPU); the JAX / equinox intensity and POD models live
in :mod:plumax.population.intensity and :mod:plumax.population.pod.
FacilityConfig
dataclass
¶
FacilityConfig(name: str = 'Default Facility', lambda_true: float = 2.0, mu: float = 2.0, sigma: float = 1.5, Q_50: float = 100.0, k: float = 0.02, duration: float = 2.0, observation_window: float = 365.0, seed: int = 42)
Immutable physical configuration of a single leaking facility.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Human-readable scenario label. |
lambda_true |
float
|
True homogeneous Poisson intensity [events day⁻¹]. |
mu |
float
|
Lognormal location parameter [ln(kg hr⁻¹)]. |
sigma |
float
|
Lognormal scale parameter [dimensionless]. |
Q_50 |
float
|
Detection threshold at P_d = 0.5 [kg hr⁻¹]. |
k |
float
|
Logistic-sigmoid steepness [hr kg⁻¹]. |
duration |
float
|
Mean event duration [hr event⁻¹]. |
observation_window |
float
|
Total observation period [days]. |
seed |
int
|
PRNG seed for reproducibility. |
ParadoxResult
dataclass
¶
ParadoxResult(config: FacilityConfig, N_true: int, N_obs: int, marks_true: NDArray, marks_obs: NDArray, E_Q_true_mc: float, E_Q_obs_mc: float, M_true_mc: float, M_obs_mc: float, E_Pd: float, M_true_analytic: float, average_overestimation_ratio: float, mass_underestimation_ratio: float, missing_mass_fraction: float, MMSF: float)
Container for a single Monte Carlo paradox simulation.
Attributes:
| Name | Type | Description |
|---|---|---|
config |
FacilityConfig
|
The scenario that was simulated. |
N_true |
int
|
Number of true (pre-thinning) events. |
N_obs |
int
|
Number of detected (post-thinning) events. |
marks_true |
NDArray
|
True event fluxes [kg hr⁻¹], shape |
marks_obs |
NDArray
|
Detected event fluxes [kg hr⁻¹], shape |
E_Q_true_mc |
float
|
Monte Carlo mean of the true fluxes [kg hr⁻¹]. |
E_Q_obs_mc |
float
|
Monte Carlo mean of the detected fluxes [kg hr⁻¹]. |
M_true_mc |
float
|
Monte Carlo true total mass [kg]. |
M_obs_mc |
float
|
Monte Carlo detected total mass [kg]. |
E_Pd |
float
|
Mark-averaged detection probability |
M_true_analytic |
float
|
Analytic expected true total mass [kg]. |
average_overestimation_ratio |
float
|
|
mass_underestimation_ratio |
float
|
|
missing_mass_fraction |
float
|
|
MMSF |
float
|
Missing-mass scale factor |
lognormal_pdf
¶
Evaluate the Lognormal probability density function.
f(Q) = (1 / (Q · σ · √(2π))) · exp(-(ln(Q) - μ)² / (2σ²))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Q
|
NDArray
|
Emission flux rates [kg hr⁻¹]. Must be > 0. Shape |
required |
mu
|
float
|
Lognormal location parameter [ln(kg hr⁻¹)]. |
required |
sigma
|
float
|
Lognormal scale parameter [dimensionless]. |
required |
Returns:
| Type | Description |
|---|---|
NDArray
|
PDF values [hr kg⁻¹], same shape as |
Source code in src/plumax/population/paradox.py
logistic_pod
¶
Parametric probability of detection (logistic sigmoid).
P_d(Q) = 1 / (1 + exp(-k · (Q - Q₅₀))), evaluated with
scipy.special.expit for a numerically stable sigmoid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Q
|
NDArray
|
Emission flux rates [kg hr⁻¹]. |
required |
Q_50
|
float
|
Detection midpoint, |
required |
k
|
float
|
Logistic steepness [hr kg⁻¹]. |
required |
Returns:
| Type | Description |
|---|---|
NDArray
|
Detection probabilities in |
Source code in src/plumax/population/paradox.py
compute_E_Pd
¶
Compute E[P_d] = ∫₀^∞ P_d(Q) · f(Q) dQ by log-space quadrature.
We integrate in log-space u = ln(Q) so the lognormal kernel becomes a Gaussian in u, truncated to [μ - 6σ, μ + 6σ].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mu
|
float
|
Lognormal location parameter [ln(kg hr⁻¹)]. |
required |
sigma
|
float
|
Lognormal scale parameter [dimensionless]. |
required |
Q_50
|
float
|
Logistic POD midpoint [kg hr⁻¹]. |
required |
k
|
float
|
Logistic POD steepness [hr kg⁻¹]. |
required |
n_quad
|
int
|
Number of trapezoid points. |
10000
|
Returns:
| Type | Description |
|---|---|
float
|
E[P_d] ∈ [0, 1]. |
Source code in src/plumax/population/paradox.py
simulate_paradox
¶
Run a single Monte Carlo realisation of the Missing Mass Paradox.
Implements the three-layer stochastic architecture:
- Draw
N_true ~ Poisson(Λ_true). - Draw
Q_i ~ LogNormal(μ, σ)fori = 1..N_true. - Thin each event independently with
Bernoulli(P_d(Q_i)).
Then computes the paradox metrics:
E[Q_obs] > E[Q_true](average overestimation);M_obs < M_true(total mass underestimation).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
FacilityConfig
|
Physical scenario parameters. |
required |
E_Pd
|
float | None
|
Pre-computed |
None
|
Returns:
| Type | Description |
|---|---|
ParadoxResult
|
Full simulation output with diagnostic metrics. |
Source code in src/plumax/population/paradox.py
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
run_scenario_grid
¶
run_scenario_grid(configs: list[FacilityConfig], n_mc_per_scenario: int = 50) -> list[ParadoxResult]
Run the paradox simulation across multiple scenarios.
Returns the median trial (by MMSF) per scenario to reduce sampling noise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
configs
|
list[FacilityConfig]
|
Scenarios to simulate. |
required |
n_mc_per_scenario
|
int
|
Number of independent Monte Carlo trials per scenario. |
50
|
Returns:
| Type | Description |
|---|---|
list[ParadoxResult]
|
One representative result per scenario. |
Source code in src/plumax/population/paradox.py
build_canonical_scenarios
¶
Six canonical test scenarios spanning the paradox regime.
Detection midpoints (Q₅₀) are set to be representative of the sparse-revisit satellite constellation actually in orbit today — Sentinel-2, Landsat-8/9, EMIT, EnMAP, PRISMA, TROPOMI, Sentinel-3. For a given overpass the least-sensitive platform dominates; stacked multi-sensor POD is on the order of 500–3000 kg/hr. GHGSat/Tanager (Q₅₀ ~ 100 kg/hr) is the hyperspectral floor but only provides sparse tasked coverage.
- Abandoned Well — Q₅₀=2000 (TROPOMI-heavy). Severe paradox.
- Active Compressor — Q₅₀=1000 (mixed constellation). Moderate.
- Super-Emitter (tasked) — Q₅₀=300 (hyperspectral tasking). Mild.
- Extreme Skew (σ=2.5) — Q₅₀=1000. A few blowouts dominate mass.
- Sharp Sensor (ideal limit) — Q₅₀=1000, k large. Near-binary threshold.
- Soft Sensor — Q₅₀=1000, k small. Gradual stacked POD.
Returns:
| Type | Description |
|---|---|
list[FacilityConfig]
|
The six scenarios, in the order above. |
Source code in src/plumax/population/paradox.py
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
fitting — POD-modified power-law NUTS fit¶
Tier V.B — POD-modified power-law fitting via NumPyro NUTS.
A Bayesian fit of the observed methane plume flux distribution to the model::
p_power(x) = C₁ · x⁻ᵅ, x_min ≤ x ≤ x_max
q(x) = Φ_LN(x; x₀, σ) (lognormal CDF = POD)
p_obs(x) = C₂ · x⁻ᵅ · q(x) (detected distribution)
with parameters (α, x₀, σ) inferred by NUTS. Data loading, plotting and
per-satellite CSV plumbing stay in the notebooks; this module exposes only
the pure-computation pieces:
- :func:
lognorm_cdf— POD curve evaluation on a grid. - :func:
power_law— un-normalised power-law PDF. - :func:
pod_powerlaw_model— the NumPyro generative model. - :func:
run_mcmc— NUTS runner returning a posterior DataFrame.
This module imports NumPyro at module level; :mod:plumax.population
binds its names lazily so importing the package stays cheap. pandas is
imported only inside :func:run_mcmc.
lognorm_cdf
¶
Lognormal CDF used as the probability-of-detection curve.
POD(x) = 1 − ½ · erfc((ln x − ln x₅₀) / (√2 · σ)).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray
|
Emission rate [kg/hr]. Must be strictly positive. Shape |
required |
x50
|
float
|
Median detection rate [kg/hr]. Must be strictly positive. |
required |
s
|
float
|
Lognormal width [dimensionless]. Must be strictly positive. |
required |
Returns:
| Type | Description |
|---|---|
NDArray
|
Probability of detection ∈ [0, 1]. Shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any of |
Source code in src/plumax/population/fitting.py
power_law
¶
pod_powerlaw_model
¶
pod_powerlaw_model(x_obs: ndarray, x_min: float, x_max: float, num_integration_points: int = 5000) -> None
NumPyro model for a POD-modified power law on observed fluxes.
Sampled parameters
alpha— power-law exponent,Uniform(1.1, 4.5)[dimensionless].x0— lognormal CDF median (POD 50 %),Uniform(1, 20000)[kg/hr].sk— lognormal CDF width,Uniform(0.1, 1.5)[dimensionless].
Log-likelihood (numerical integration in log-space for I(θ))::
ln L = −α · Σᵢ ln xᵢ + Σᵢ ln q(xᵢ) − N · ln I(θ)
I(θ) = ∫_{x_min}^{x_max} q(x) · x⁻ᵅ dx
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_obs
|
ndarray
|
Observed emission rates [kg/hr]. Shape |
required |
x_min
|
float
|
Lower integration bound [kg/hr]. |
required |
x_max
|
float
|
Upper integration bound [kg/hr]. |
required |
num_integration_points
|
int
|
Trapezoidal grid size for I(θ). Default 5000. |
5000
|
Source code in src/plumax/population/fitting.py
run_mcmc
¶
run_mcmc(data_values: NDArray, *, x_min: float = X_MIN_DEFAULT, x_max: float = X_MAX_DEFAULT, num_warmup: int = 500, num_samples: int = 1000, num_chains: int = 2, seed: int = 20, print_summary: bool = False) -> pd.DataFrame
Run NumPyro NUTS on the POD-modified power-law model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_values
|
NDArray
|
Observed Q values [kg/hr]. Must be strictly positive and within the
[x_min, x_max] domain of the power law. Shape |
required |
x_min
|
float
|
Lower power-law support bound [kg/hr]. |
X_MIN_DEFAULT
|
x_max
|
float
|
Upper power-law support bound [kg/hr]. |
X_MAX_DEFAULT
|
num_warmup
|
int
|
NUTS warmup iterations per chain. Defaults are tuned for a notebook-grade fit (~seconds on CPU); bump to 2000-4000 for a publication-grade run. |
500
|
num_samples
|
int
|
Posterior samples per chain after warmup. |
1000
|
num_chains
|
int
|
Number of independent MCMC chains. |
2
|
seed
|
int
|
PRNG seed. |
20
|
print_summary
|
bool
|
If True, call |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
A pandas |
DataFrame
|
with columns |
DataFrame
|
[dimensionless]. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/plumax/population/fitting.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |