Gaussian plume¶
Steady-state Gaussian plume with Briggs dispersion, plus NumPyro emission-rate inference. Tier I.
gauss_plume
¶
Steady-state Gaussian plume model (JAX + NumPyro).
Submodules
- dispersion — Briggs σ_y(x), σ_z(x) coefficients (A-F stability classes)
- plume — coordinate rotation + JIT forward model + xarray wrapper
- plume_rise — Briggs plume rise Δh(x) and the effective release height
- inference — NumPyro Bayesian inference for Q (and optional stability)
- observation — column + averaging-kernel observation shared by every tier
- likelihoods — Gaussian, Student-t and heteroscedastic observation noise
- multi_source — masked-K source slots and their posterior
The inference submodule is imported lazily on first attribute access so
that from plumax import gauss_plume does not pull in NumPyro
for consumers of only the forward model.
plume_concentration_vmap
module-attribute
¶
plume_concentration_vmap = vmap(plume_concentration, in_axes=(0, 0, 0, None, None, None, None, None, None, None), out_axes=0)
Vectorised over receptor locations — x, y, z batched along axis 0.
GaussianNoise
¶
Bases: Noise
Homoscedastic Gaussian noise, ε ~ 𝒩(0, σ²).
log p(y | ŷ) = −½ ((y − ŷ)/σ)² − log σ − ½ log 2π.
The Tier I models default to this with σ = obs_noise_std, so their
numbers are unchanged.
Attributes:
| Name | Type | Description |
|---|---|---|
std |
float | Float[Array, '*#obs']
|
Noise standard deviation σ > 0, in the units of y; a scalar or an array that broadcasts against the observations. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
Examples:
The log-density at the mean of a unit Gaussian is −½ log 2π:
>>> import jax.numpy as jnp
>>> noise = GaussianNoise(std=1.0)
>>> value = noise.log_prob(jnp.zeros(3), jnp.zeros(3))
>>> value.shape
(3,)
>>> bool(jnp.allclose(value, -0.5 * jnp.log(2.0 * jnp.pi)))
True
A non-positive scale is rejected:
>>> GaussianNoise(std=0.0)
Traceback (most recent call last):
...
ParameterError: std must be > 0, got 0.0
HeteroscedasticNoise
¶
Bases: Noise
Per-observation Gaussian noise, ε_i ~ 𝒩(0, σ_i²), from a retrieval-error map.
log p(y_i | ŷ_i) = −½ ((y_i − ŷ_i)/σ_i)² − log σ_i − ½ log 2π.
Unlike :class:GaussianNoise, σ must give one value per observation, and
a mismatch with the observations is an error rather than a broadcast.
Attributes:
| Name | Type | Description |
|---|---|---|
std |
Float[Array, ' obs']
|
Per-observation standard deviation σ_i > 0, in the units of y. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If any |
Examples:
Doubling σ_i halves the weight of a residual:
>>> import jax.numpy as jnp
>>> noise = HeteroscedasticNoise(std=jnp.array([1.0, 2.0]))
>>> value = noise.log_prob(jnp.array([2.0, 4.0]), jnp.zeros(2))
>>> bool(jnp.allclose(value[0] - value[1], jnp.log(2.0)))
True
Build it from variances, e.g. Instrument.observation_variance():
from_variance
classmethod
¶
The noise with σ_i = √(variance_i).
Noise
¶
Bases: Module
Observation-noise model p(y | ŷ) for a Tier I likelihood.
Subclasses define :meth:distribution (the NumPyro distribution, for
models) and :meth:log_prob (plain JAX, for losses); they must agree.
StudentTNoise
¶
Bases: Noise
Heavy-tailed Student-t noise, ε ~ t_ν(0, s), for outliers near the detection floor.
log p(y | ŷ) = log Γ((ν+1)/2) − log Γ(ν/2) − ½ log(ν π) − log s − (ν+1)/2 · log(1 + ((y − ŷ)/s)² / ν).
ν ≈ 5 is the Tier I design's default for low-SNR retrievals; as ν → ∞ it
tends to :class:GaussianNoise with σ = s.
Attributes:
| Name | Type | Description |
|---|---|---|
scale |
float | Float[Array, '*#obs']
|
Scale s > 0, in the units of y; a scalar or an array that broadcasts against the observations. The standard deviation is s √(ν/(ν − 2)) for ν > 2. |
df |
float
|
Degrees of freedom ν > 0. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
Examples:
A 10-scale outlier costs far less than under a Gaussian:
>>> import jax.numpy as jnp
>>> obs, mean = jnp.array([10.0]), jnp.array([0.0])
>>> student = StudentTNoise(scale=1.0).log_prob(obs, mean)
>>> gaussian = GaussianNoise(std=1.0).log_prob(obs, mean)
>>> bool(student[0] > gaussian[0] + 30.0)
True
Large ν recovers the Gaussian:
>>> near = StudentTNoise(scale=1.0, df=1.0e8).log_prob(obs, mean)
>>> bool(jnp.allclose(near, gaussian, rtol=1.0e-6))
True
GolderDispersion
¶
GolderDispersion(inverse_obukhov_length: Float[Array, ''] | float, z0: float, interpolate: bool = False)
Bases: Module
PG σ_y, σ_z with the class chosen from Monin–Obukhov scales (Golder 1972).
σ_i(x) = σ_i^PG(x; class(1/L, z₀)), i ∈ {y, z}, with the Briggs–PG curves
of :func:~plumax.gauss_plume.calculate_briggs_dispersion. With
interpolate=True the class index s = :func:golder_stability_index is
continuous and σ_i(x) = (1 − w) σ_i^PG(x; ⌊s⌋) + w σ_i^PG(x; ⌊s⌋ + 1),
w = s − ⌊s⌋, which is differentiable in L (a plumax modelling choice, not
part of Golder). Pass it as dispersion_params to
:func:~plumax.gauss_plume.plume_concentration or as stability_class
to :func:~plumax.gauss_plume.simulate_plume.
Attributes:
| Name | Type | Description |
|---|---|---|
inverse_obukhov_length |
Float[Array, '']
|
1/L [m⁻¹] of the column; zero is neutral. |
z0 |
float
|
Roughness length z₀ > 0 [m]; clamped to :data: |
interpolate |
bool
|
Blend neighbouring PG classes linearly in 1/L (default
|
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
Examples:
Neutral air reproduces PG class D exactly:
>>> import jax.numpy as jnp
>>> from plumax.gauss_plume import calculate_briggs_dispersion
>>> from plumax.gauss_plume import get_dispersion_params
>>> distance = jnp.array([100.0, 1000.0])
>>> neutral = GolderDispersion(inverse_obukhov_length=0.0, z0=0.1)
>>> sigma_y, sigma_z = neutral(distance)
>>> pg_y, pg_z = calculate_briggs_dispersion(distance, get_dispersion_params("D"))
>>> bool(jnp.allclose(sigma_y, pg_y) & jnp.allclose(sigma_z, pg_z))
True
The interpolated σ moves smoothly with L inside a class:
>>> import jax
>>> def sigma_z_at_1km(inverse_length):
... dispersion = GolderDispersion(inverse_length, z0=0.1, interpolate=True)
... return dispersion(jnp.array(1000.0))[1]
>>> bool(jax.grad(sigma_z_at_1km)(-1.0 / 50.0) < 0.0)
True
Build it from a met column's surface-layer diagnosis:
>>> from plumax.les_fvm.grid import make_grid
>>> from plumax.met import MetField, MoninObukhov
>>> grid = make_grid(
... (0.0, 400.0, 4), (0.0, 400.0, 4), (0.0, 200.0, 4), dtype=jnp.float64
... )
>>> shape = (1, 4, 4, 4)
>>> field = MetField(
... plume_grid=grid, times=jnp.zeros(1), u=jnp.full(shape, 5.0),
... v=jnp.zeros(shape), w=jnp.zeros(shape),
... temperature=jnp.full(shape, 300.0), pressure=jnp.full(shape, 1.0e5),
... pbl_height=jnp.full((1, 4, 4), 800.0),
... )
>>> layer = MoninObukhov()(field, z0=0.1, sensible_heat_flux=0.0)
>>> GolderDispersion.from_surface_layer(layer, z0=0.1, it=0, iy=1, ix=2).z0
0.1
References
Golder, D. (1972). Relations among stability parameters in the surface
layer. Boundary-Layer Meteorology 3, 47–58.
US EPA (2024). MMIF v4.1.1, pasquill_gifford.f90 (pg_golder).
Source code in src/plumax/gauss_plume/_src/mo_dispersion.py
from_surface_layer
classmethod
¶
from_surface_layer(layer: SurfaceLayer, *, z0: float, it: int, iy: int, ix: int, interpolate: bool = False) -> GolderDispersion
Golder dispersion at one column of a :class:plumax.met.SurfaceLayer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer
|
SurfaceLayer
|
Surface-layer scales from :class: |
required |
z0
|
float
|
The roughness length z₀ [m] the layer was diagnosed with. |
required |
it
|
int
|
Met time index. |
required |
iy
|
int
|
Analysis y index. |
required |
ix
|
int
|
Analysis x index. |
required |
interpolate
|
bool
|
See the class attribute. |
False
|
Returns:
| Type | Description |
|---|---|
GolderDispersion
|
The dispersion model for that column. |
Source code in src/plumax/gauss_plume/_src/mo_dispersion.py
MaskedSources
¶
Bases: Module
K_max source slots with emission rates, locations and activity indicators.
c(𝐬) = Σ_k a_k Q_k c₁(𝐬; x_k, y_k).
Attributes:
| Name | Type | Description |
|---|---|---|
emission_rate |
Float[Array, ' source']
|
Q_k [kg s⁻¹] per slot. |
x |
Float[Array, ' source']
|
Source x_k [m] per slot. |
y |
Float[Array, ' source']
|
Source y_k [m] per slot. |
active |
Float[Array, ' source']
|
Activity a_k per slot, 1 on and 0 off (a float, so a relaxed indicator in [0, 1] works too). |
Examples:
Two slots, one on, one off: the field is the first source's alone.
>>> import jax.numpy as jnp
>>> from plumax.gauss_plume.dispersion import BRIGGS_DISPERSION_PARAMS
>>> sources = MaskedSources(
... emission_rate=jnp.array([0.1, 5.0]), x=jnp.array([0.0, 50.0]),
... y=jnp.array([0.0, 50.0]), active=jnp.array([1.0, 0.0]),
... )
>>> sources.active_count().tolist()
1.0
>>> receptors = (jnp.array([300.0]), jnp.array([0.0]), jnp.array([1.0]))
>>> params = BRIGGS_DISPERSION_PARAMS["D"]
>>> field = sources.concentration(
... receptors, source_z=2.0, wind_u=5.0, wind_v=0.0, dispersion_params=params
... )
>>> single = plume_concentration(*receptors, 0.0, 0.0, 2.0, 5.0, 0.0, 0.1, params)
>>> bool(jnp.allclose(field, single))
True
active_count
¶
unit_responses
¶
unit_responses(receptor_coords: tuple[Float[Array, ' obs'], Float[Array, ' obs'], Float[Array, ' obs']], *, source_z: float | Float[Array, ''], wind_u: float | Float[Array, ''], wind_v: float | Float[Array, ''], dispersion_params: Float[Array, ' param']) -> Float[Array, 'source obs']
Unit-rate plume c₁(𝐬_i; x_k, y_k) of every slot at every receptor.
Source code in src/plumax/gauss_plume/_src/multi_source.py
concentration
¶
concentration(receptor_coords: tuple[Float[Array, ' obs'], Float[Array, ' obs'], Float[Array, ' obs']], *, source_z: float | Float[Array, ''], wind_u: float | Float[Array, ''], wind_v: float | Float[Array, ''], dispersion_params: Float[Array, ' param']) -> Float[Array, ' obs']
c(𝐬_i) = Σ_k a_k Q_k c₁(𝐬_i; x_k, y_k) [kg m⁻³].
Source code in src/plumax/gauss_plume/_src/multi_source.py
MultiSourcePosterior
dataclass
¶
MultiSourcePosterior(emission_rate: ndarray, x: ndarray, y: ndarray, active: ndarray, background: ndarray)
Posterior draws of a masked-K source model.
Attributes:
| Name | Type | Description |
|---|---|---|
emission_rate |
ndarray
|
Q_k per draw and slot [kg s⁻¹], shape |
x |
ndarray
|
x_k per draw and slot [m]. |
y |
ndarray
|
y_k per draw and slot [m]. |
active |
ndarray
|
a_k per draw and slot (bool). |
background |
ndarray
|
Background per draw [kg m⁻³], shape |
Examples:
Three draws of two slots:
>>> import numpy as np
>>> posterior = MultiSourcePosterior(
... emission_rate=np.array([[0.1, 0.5], [0.2, 0.6], [0.3, 0.7]]),
... x=np.zeros((3, 2)), y=np.zeros((3, 2)),
... active=np.array([[True, False], [True, True], [True, False]]),
... background=np.zeros(3),
... )
>>> posterior.count_probabilities().round(3).tolist()
[0.0, 0.667, 0.333]
>>> summary = posterior.per_source(1)
>>> summary.probability_active, summary.emission_rate
(0.3333333333333333, 0.6)
active_count
¶
count_probabilities
¶
P(K = k | y) for k = 0, …, K_max, shape (K_max + 1,).
saturated_fraction
¶
P(K = K_max | y): the mass at the cap, which drives the RJMCMC promotion.
per_source
¶
Summary of slot k over the draws where it is active.
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
Source code in src/plumax/gauss_plume/_src/multi_source.py
SourceSummary
dataclass
¶
SourceSummary(emission_rate: float, emission_std: float, x: float, y: float, probability_active: float)
Posterior summary of one slot, conditioned on the slot being active.
It satisfies :class:plumax.population.PerEventPosterior, so
event_from_posterior(summary, x=summary.x, y=summary.y, …) builds the
slot's catalogue event.
Attributes:
| Name | Type | Description |
|---|---|---|
emission_rate |
float
|
Posterior mean of Q_k over draws with a_k = 1 [kg s⁻¹]; NaN when the slot is never active. |
emission_std |
float
|
Posterior standard deviation of Q_k over those draws. |
x |
float
|
Posterior mean of x_k over those draws [m]. |
y |
float
|
Posterior mean of y_k over those draws [m]. |
probability_active |
float
|
Posterior probability P(a_k = 1 | y). |
ColumnObservation
¶
Bases: Module
Averaging-kernel-weighted column at a set of receptors.
y_i = A_i Σ_k h_k c_{k,i}.
Attributes:
| Name | Type | Description |
|---|---|---|
layer_weights |
Float[Array, ' level']
|
Level weights h_k, the layer thicknesses [m] for a column integral. |
averaging_kernel |
Float[Array, ' obs'] | None
|
Per-receptor scalar averaging kernel A_i. |
receptor_index |
Int[Array, 'obs 2'] | None
|
|
Examples:
Two levels 10 m thick, every column of a uniform field:
>>> import jax.numpy as jnp
>>> observation = ColumnObservation(layer_weights=jnp.full(2, 10.0))
>>> observation.from_field(jnp.ones((2, 3, 4))).shape
(12,)
Two receptors with an averaging kernel:
>>> observation = ColumnObservation(
... layer_weights=jnp.full(2, 10.0),
... averaging_kernel=jnp.array([1.0, 0.5]),
... receptor_index=jnp.array([[0, 0], [2, 3]]),
... )
>>> observation.from_field(jnp.ones((2, 3, 4))).tolist()
[20.0, 10.0]
Trapezoid weights for profiles on irregular levels:
>>> levels = jnp.array([0.0, 10.0, 30.0])
>>> ColumnObservation.trapezoid(levels).layer_weights.tolist()
[5.0, 15.0, 10.0]
trapezoid
classmethod
¶
trapezoid(levels: Float[Array, ' level'], averaging_kernel: Float[Array, ' obs'] | None = None) -> ColumnObservation
Column over profiles on strictly increasing levels, trapezoid rule.
h_0 = Δz_0 / 2, h_k = (Δz_{k-1} + Δz_k) / 2, h_{K-1} = Δz_{K-2} / 2,
so Σ_k h_k c_k is jnp.trapezoid(c, x=levels).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
levels
|
Float[Array, ' level']
|
Level heights z_k [m]. |
required |
averaging_kernel
|
Float[Array, ' obs'] | None
|
Per-receptor scalar averaging kernel A_i. |
None
|
Returns:
| Type | Description |
|---|---|
ColumnObservation
|
The observation, with no receptor index (it takes profiles). |
Source code in src/plumax/gauss_plume/_src/observation.py
for_grid
classmethod
¶
for_grid(plume_grid: PlumeGrid3D, *, instrument: Instrument | None = None, receptor_index: Int[Array, 'obs 2'] | None = None) -> ColumnObservation
Column over a les_fvm analysis grid, midpoint rule (h_k = Δz).
An instrument places each receptor in the T-cell that contains it and
brings its averaging kernel. Without one, receptor_index (or every
column) is observed with the identity kernel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plume_grid
|
PlumeGrid3D
|
The analysis grid the field lives on. |
required |
instrument
|
Instrument | None
|
Receptors |
None
|
receptor_index
|
Int[Array, 'obs 2'] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
ColumnObservation
|
The observation for interior fields of shape |
Raises:
| Type | Description |
|---|---|
DomainError
|
If a receptor lies outside the grid. |
ValueError
|
If both |
Source code in src/plumax/gauss_plume/_src/observation.py
column
¶
Apply the observation to per-receptor profiles c_{k,i}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profiles
|
Float[Array, 'obs level']
|
Concentration at each receptor and level. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' obs']
|
The observation y_i. |
Source code in src/plumax/gauss_plume/_src/observation.py
from_field
¶
Apply the observation to an interior field on the analysis grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
concentration
|
Float[Array, 'level y x']
|
Interior concentration c, ghost rings stripped. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' obs']
|
The observation y_i at each receptor. |
Source code in src/plumax/gauss_plume/_src/observation.py
BriggsPlumeRise
¶
Bases: Module
Briggs plume rise Δh(x) of a stack plume, from its buoyancy and momentum.
Δh(x) = max(Δh_b(x), Δh_m(x)), with the buoyant and momentum rises of the module docstring.
Algorithm
- Fluxes F_b = g v_s d² (T_s − T_a)/(4 T_s), floored at 0, and F_m = v_s² d² T_a/(4 T_s).
- Buoyant rise: the 2/3 law 1.6 F_b^{1/3} x^{2/3}/u, capped at the class's final rise.
- Momentum rise: the jet law 1.44 d (v_s/u)^{2/3} (x/d)^{1/3}, capped at 3 d v_s/u, and in stable air also at 1.5 (F_m/u)^{1/3} s^{−1/6}.
- Take the larger of the two.
Attributes:
| Name | Type | Description |
|---|---|---|
exit_velocity |
float | Float[Array, '']
|
Stack exit velocity v_s ≥ 0 [m s⁻¹]. |
exit_temperature |
float | Float[Array, '']
|
Stack gas temperature T_s > 0 [K]. |
stack_diameter |
float | Float[Array, '']
|
Inside stack-top diameter d > 0 [m]. |
ambient_temperature |
float | Float[Array, '']
|
Ambient air temperature T_a > 0 [K]. |
stability_class |
str
|
Pasquill–Gifford class |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If the class is not A–F or a parameter is out of range. |
Examples:
A hot stack in neutral air: past x_f the rise is the final rise.
>>> import jax.numpy as jnp
>>> rise = BriggsPlumeRise(
... exit_velocity=10.0, exit_temperature=400.0, stack_diameter=2.0,
... ambient_temperature=290.0, stability_class="D",
... )
>>> heights = rise(jnp.array([100.0, 5000.0, 20000.0]), wind_speed=5.0)
>>> heights.shape
(3,)
>>> bool(heights[1] == heights[2])
True
A cold, still stack does not rise:
>>> still = BriggsPlumeRise(
... exit_velocity=0.0, exit_temperature=290.0, stack_diameter=2.0,
... ambient_temperature=290.0,
... )
>>> still(jnp.array([500.0]), wind_speed=5.0).tolist()
[0.0]
Classes outside A–F are rejected:
>>> BriggsPlumeRise(
... exit_velocity=1.0, exit_temperature=300.0, stack_diameter=1.0,
... ambient_temperature=290.0, stability_class="G",
... )
Traceback (most recent call last):
...
ParameterError: stability_class must be one of A-F, got 'G'
References
Briggs, G. A. (1969). Plume Rise. USAEC TID-25075 (Eqs. 4.19b, 4.28, 4.33, 5.2). U.S. EPA, User's Guide for the Industrial Source Complex (ISC3) Dispersion Models, Vol. II, §1.1.4 (after Briggs 1971, 1975).
buoyancy_flux
¶
F_b = g v_s d² (T_s − T_a)/(4 T_s) [m⁴ s⁻³], floored at 0 (no sinking).
Source code in src/plumax/gauss_plume/_src/plume_rise.py
momentum_flux
¶
F_m = v_s² d² T_a/(4 T_s) [m⁴ s⁻²].
Source code in src/plumax/gauss_plume/_src/plume_rise.py
stability_parameter
¶
s = (g / T_a) ∂θ/∂z [s⁻²] of the stable classes; 0 for A–D.
Source code in src/plumax/gauss_plume/_src/plume_rise.py
EmissionRateSamples
¶
Bases: dict
NUTS draws by site name, with the per-event payload Tier V reads.
A dict of posterior arrays (samples["emission_rate"] is unchanged)
that also satisfies population.PerEventPosterior: the posterior mean and
standard deviation of Q, resampled draws of Q, and the log-density of the
emission-rate prior the draws were made under,
Q ~ LogNormal(μ, σ²) moment-matched to (prior_mean, prior_std).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
draws
|
dict[str, NDArray]
|
Posterior arrays keyed by site name. |
required |
prior_mean
|
float
|
Real-scale prior mean of Q [kg/s]. |
required |
prior_std
|
float
|
Real-scale prior standard deviation of Q [kg/s]. |
required |
Source code in src/plumax/gauss_plume/inference.py
calculate_briggs_dispersion
¶
Evaluate σ_y(x), σ_z(x) from Briggs parameters.
σ_i(x) = a_i · x · (1 + b_i · x)^{c_i} for i ∈ {y, z}.
Parameters¶
distance : jnp.ndarray
Downwind distance from source [m]. Negative / zero values are clamped
to 1 m so that the coefficient evaluation stays finite; upwind points
are masked elsewhere in the plume model.
params : jnp.ndarray, shape (6,)
[a_y, b_y, c_y, a_z, b_z, c_z].
Returns¶
sigma_y : jnp.ndarray
Lateral dispersion coefficient [m], same shape as distance.
sigma_z : jnp.ndarray
Vertical dispersion coefficient [m], same shape as distance.
Source code in src/plumax/gauss_plume/dispersion.py
get_dispersion_params
¶
Return the Briggs parameter vector for a stability class.
Parameters¶
stability_class : str
One of 'A', 'B', 'C', 'D', 'E', 'F'.
Returns¶
params : jnp.ndarray, shape (6,)
[a_y, b_y, c_y, a_z, b_z, c_z].
Raises¶
ValueError
If stability_class is not one of A-F.
Source code in src/plumax/gauss_plume/dispersion.py
golder_class_boundaries
¶
Inverse Obukhov lengths at Golder's five PG class boundaries.
(1/L)_k = (ln z₀ − d_k) / c_k for the A/B, B/C, C/D, D/E and E/F
boundaries, with z₀ clamped to :data:GOLDER_Z0_RANGE.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z0
|
float
|
Roughness length z₀ > 0 [m]. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' boundary']
|
The five boundaries [m⁻¹], increasing from A/B (most negative) to E/F. |
Examples:
At z₀ = 0.1 m the unstable boundaries sit at L ≈ −10.5, −30.4 and −104 m, the stable ones at L ≈ 112 and 26.9 m:
>>> import jax.numpy as jnp
>>> boundaries = golder_class_boundaries(0.1)
>>> [round(float(1.0 / b), 1) for b in boundaries]
[-10.5, -30.4, -104.1, 111.6, 26.9]
z₀ beyond 0.5 m is clamped:
Source code in src/plumax/gauss_plume/_src/mo_dispersion.py
golder_stability_class
¶
golder_stability_class(inverse_obukhov_length: Float[Array, '*shape'] | float, z0: float) -> Int[Array, '*shape']
Golder's (1972) PG class for 1/L and z₀, as an index 0 (A) … 5 (F).
The class is the number of boundaries (1/L)_k strictly below 1/L, so a value on a boundary goes to the less stable class and 1/L = 0 is D.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inverse_obukhov_length
|
Float[Array, '*shape'] | float
|
1/L [m⁻¹]; zero is neutral, negative unstable. |
required |
z0
|
float
|
Roughness length z₀ > 0 [m]. |
required |
Returns:
| Type | Description |
|---|---|
Int[Array, '*shape']
|
The class index into :data: |
Examples:
Over z₀ = 0.1 m, L = −20 m is B, neutral is D and L = 50 m is E:
>>> import jax.numpy as jnp
>>> inverse = jnp.array([-1.0 / 20.0, 0.0, 1.0 / 50.0])
>>> golder_stability_class(inverse, 0.1).tolist()
[1, 3, 4]
Source code in src/plumax/gauss_plume/_src/mo_dispersion.py
golder_stability_index
¶
golder_stability_index(inverse_obukhov_length: Float[Array, '*shape'] | float, z0: float) -> Float[Array, '*shape']
Continuous PG class index from 1/L and z₀, piecewise linear in 1/L.
s(1/L) interpolates linearly through the knots (1/L, s) = ((1/L)_AB − Δ_A, 0), ((1/L)_AB, 0.5), ((1/L)_BC, 1.5), ((1/L)_CD, 2.5), (0, 3), ((1/L)_DE, 3.5), ((1/L)_EF, 4.5), ((1/L)_EF + Δ_F, 5), and is clamped to [0, 5] outside them, with Δ_A = ((1/L)_BC − (1/L)_AB)/2 and Δ_F = ((1/L)_EF − (1/L)_DE)/2 (the half-width of the neighbouring class).
This is a plumax modelling choice, not part of Golder (1972): Golder gives
only the class boundaries. Each boundary maps to the half-integer between
its two classes, so round of the index (ties to the less stable class)
is the hard class of :func:golder_stability_class, and neutral maps to
exactly 3 (class D). Its derivative in 1/L is non-zero everywhere except
deep inside A (1/L < (1/L)_AB − Δ_A) and F (1/L > (1/L)_EF + Δ_F).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inverse_obukhov_length
|
Float[Array, '*shape'] | float
|
1/L [m⁻¹]; zero is neutral, negative unstable. |
required |
z0
|
float
|
Roughness length z₀ > 0 [m] (a scalar). |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*shape']
|
The class index s ∈ [0, 5], 0 for A and 5 for F. |
Examples:
Boundaries land on half-integers and neutral on D:
>>> import jax.numpy as jnp
>>> boundaries = golder_class_boundaries(0.1)
>>> golder_stability_index(boundaries, 0.1).tolist()
[0.5, 1.5, 2.5, 3.5, 4.5]
>>> float(golder_stability_index(0.0, 0.1))
3.0
Source code in src/plumax/gauss_plume/_src/mo_dispersion.py
plume_concentration
¶
plume_concentration(x: ndarray, y: ndarray, z: ndarray, source_x: float, source_y: float, source_z: float, wind_u: float, wind_v: float, emission_rate: float, dispersion_params: ndarray | GolderDispersion, plume_rise: BriggsPlumeRise | None = None) -> jnp.ndarray
Evaluate the steady-state Gaussian plume concentration.
Computes the mass concentration [kg/m³] at receptor points (x, y, z)
in the fixed reference frame. Coordinates are rotated into the
wind-aligned frame internally, upwind points are masked to zero, and
ground reflection is applied.
Parameters¶
x, y, z : jnp.ndarray
Receptor coordinates in the fixed frame [m]. Broadcast-compatible.
source_x, source_y, source_z : float
Source location [m]. source_z is the emission height above ground.
wind_u, wind_v : float
Wind velocity components [m/s]. The total wind speed is clamped
below to :data:MIN_WIND_SPEED for numerical stability.
emission_rate : float
Continuous emission rate Q [kg/s].
dispersion_params : jnp.ndarray, shape (6,), or GolderDispersion
[a_y, b_y, c_y, a_z, b_z, c_z] (Briggs parameters of one PG
class), or a :class:~plumax.gauss_plume.GolderDispersion that picks
the PG class from the Obukhov length and roughness length.
plume_rise : BriggsPlumeRise, optional
Plume rise Δh(x'). When given, the release height at each receptor is
the effective height source_z + Δh(x') with Δh evaluated at the
receptor's downwind distance and the (clamped) wind speed; it is
differentiable in the stack parameters. Default None keeps the
fixed height source_z.
Returns¶
concentration : jnp.ndarray Mass concentration [kg/m³], same shape as the broadcast of the input coordinates. Zero at upwind locations (x' ≤ 0).
Source code in src/plumax/gauss_plume/plume.py
103 104 105 106 107 108 109 110 111 112 113 114 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 | |
rotate_to_wind_frame
¶
rotate_to_wind_frame(x: ndarray, y: ndarray, source_x: float, source_y: float, wind_u: float, wind_v: float) -> tuple[jnp.ndarray, jnp.ndarray]
Rotate fixed-frame coordinates into the wind-aligned frame.
The wind-aligned frame has its origin at the source, its x'-axis pointing
downwind, and its y'-axis pointing crosswind (90° counter-clockwise from
x', following the right-hand rule). The rotation angle is
θ = atan2(wind_v, wind_u) [rad].
Parameters¶
x, y : jnp.ndarray Receptor coordinates in the fixed frame [m]. Must have matching shapes (or be broadcast-compatible). source_x, source_y : float Source location in the fixed frame [m]. wind_u, wind_v : float Wind velocity components [m/s].
Returns¶
x_wind : jnp.ndarray Downwind coordinate x' [m] (> 0 downwind, < 0 upwind of source). y_wind : jnp.ndarray Crosswind coordinate y' [m].
Source code in src/plumax/gauss_plume/plume.py
simulate_plume
¶
simulate_plume(emission_rate: float, source_location: tuple[float, float, float], wind_speed: float, wind_direction: float, stability_class: str | GolderDispersion, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], domain_z: tuple[float, float, int], background_conc: float = 0.0, plume_rise: BriggsPlumeRise | None = None) -> xr.Dataset
Simulate a steady-state Gaussian plume on a 3-D grid.
Wraps :func:plume_concentration with grid construction, wind-from-angle
to (u, v) conversion, and column integration, returning an
:class:xarray.Dataset suitable for plotting / I/O.
Wind-direction convention follows the meteorological "from" convention:
wind_direction = 270° is a wind from the west (flowing east);
wind_direction = 0° is a wind from the north (flowing south).
u = -wind_speed · sin(θ), v = -wind_speed · cos(θ),
with θ = wind_direction in radians.
Parameters¶
emission_rate : float
Continuous emission rate Q [kg/s]. Must be strictly positive.
source_location : tuple of float, (3,)
(x, y, z) source coordinates [m]. z ≥ 0.
wind_speed : float
Wind speed magnitude [m/s]. Must be strictly positive.
wind_direction : float
Wind direction [degrees from North, meteorological "from" convention].
stability_class : str or GolderDispersion
One of 'A', ..., 'F', or a
:class:~plumax.gauss_plume.GolderDispersion that picks the class from
the Obukhov length and roughness length.
domain_x, domain_y, domain_z : tuple of (float, float, int)
(start, stop, n_points) for each axis. start < stop and
n_points ≥ 2.
background_conc : float
Additive background concentration [kg/m³]. Default 0.0.
plume_rise : BriggsPlumeRise, optional
Plume rise added to the source height z (see
:func:plume_concentration). Default None: no rise.
Returns¶
ds : xarray.Dataset
Variables: concentration (x, y, z) [kg/m³],
column_concentration (x, y) [kg/m²].
Raises¶
ValueError On invalid stability class, non-positive emission rate, non-positive wind speed, malformed domain tuples, or negative source height.
Source code in src/plumax/gauss_plume/plume.py
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 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 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | |
briggs_plume_rise
¶
briggs_plume_rise(x: Float[Array, '*obs'], *, exit_velocity: float | Float[Array, ''], exit_temperature: float | Float[Array, ''], stack_diameter: float | Float[Array, ''], ambient_temperature: float | Float[Array, ''], wind_speed: float | Float[Array, ''], stability_class: str = 'D') -> Float[Array, '*obs']
Briggs plume rise Δh(x) [m]; see :class:BriggsPlumeRise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Float[Array, '*obs']
|
Downwind distance [m]. |
required |
exit_velocity
|
float | Float[Array, '']
|
Stack exit velocity v_s [m s⁻¹]. |
required |
exit_temperature
|
float | Float[Array, '']
|
Stack gas temperature T_s [K]. |
required |
stack_diameter
|
float | Float[Array, '']
|
Stack diameter d [m]. |
required |
ambient_temperature
|
float | Float[Array, '']
|
Ambient temperature T_a [K]. |
required |
wind_speed
|
float | Float[Array, '']
|
Wind speed u at the stack top [m s⁻¹]. |
required |
stability_class
|
str
|
Pasquill–Gifford class |
'D'
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*obs']
|
Δh(x), with the final rise past x_f. |
Examples:
The final rise of a buoyant plume in neutral air (F_b < 55):
>>> import jax.numpy as jnp
>>> rise = briggs_plume_rise(
... jnp.array([1.0e5]), exit_velocity=10.0, exit_temperature=400.0,
... stack_diameter=2.0, ambient_temperature=290.0, wind_speed=5.0,
... )
>>> flux = 9.80665 * 10.0 * 4.0 * 110.0 / 1600.0
>>> final = 1.6 * flux ** (1 / 3) * (49.0 * flux ** 0.625) ** (2 / 3) / 5.0
>>> bool(jnp.allclose(rise, final))
True
Source code in src/plumax/gauss_plume/_src/plume_rise.py
effective_height
¶
effective_height(x: Float[Array, '*obs'], *, stack_height: float | Float[Array, ''], exit_velocity: float | Float[Array, ''], exit_temperature: float | Float[Array, ''], stack_diameter: float | Float[Array, ''], ambient_temperature: float | Float[Array, ''], wind_speed: float | Float[Array, ''], stability_class: str = 'D') -> Float[Array, '*obs']
Effective release height H_eff(x) = H_stack + Δh(x) [m].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Float[Array, '*obs']
|
Downwind distance [m]. |
required |
stack_height
|
float | Float[Array, '']
|
Physical stack height H_stack [m]. |
required |
exit_velocity
|
float | Float[Array, '']
|
Stack exit velocity v_s [m s⁻¹]. |
required |
exit_temperature
|
float | Float[Array, '']
|
Stack gas temperature T_s [K]. |
required |
stack_diameter
|
float | Float[Array, '']
|
Stack diameter d [m]. |
required |
ambient_temperature
|
float | Float[Array, '']
|
Ambient temperature T_a [K]. |
required |
wind_speed
|
float | Float[Array, '']
|
Wind speed u at the stack top [m s⁻¹]. |
required |
stability_class
|
str
|
Pasquill–Gifford class |
'D'
|
Returns:
| Type | Description |
|---|---|
Float[Array, '*obs']
|
H_eff at each x. |
Examples:
At the stack itself the plume has not risen yet:
>>> import jax.numpy as jnp
>>> effective_height(
... jnp.array([0.0]), stack_height=30.0, exit_velocity=10.0,
... exit_temperature=400.0, stack_diameter=2.0,
... ambient_temperature=290.0, wind_speed=5.0,
... ).tolist()
[30.0]
Source code in src/plumax/gauss_plume/_src/plume_rise.py
gaussian_plume_model
¶
gaussian_plume_model(observations: ndarray | None = None, receptor_coords: tuple[ndarray, ndarray, ndarray] | None = None, source_location: tuple[float, float, float] | None = None, wind_u: float | None = None, wind_v: float | None = None, stability_class: str = 'C', prior_emission_rate_mean: float = 0.1, prior_emission_rate_std: float = 0.05, infer_stability: bool = False, background_prior_std: float = 5e-07, obs_noise_std: float = 5e-07, noise: Noise | None = None) -> jnp.ndarray
NumPyro model for the steady-state Gaussian plume.
Priors
emission_rate ~ LogNormal(μ_log, σ_log) [kg/s] stability_idx ~ Categorical(1/6, ..., 1/6) (optional) [A..F] background ~ HalfNormal(σ_bg) [kg/m³]
where (μ_log, σ_log) are the log-space parameters derived from the
requested real-scale (prior_emission_rate_mean, prior_emission_rate_std)
via the method-of-moments inversion in :func:_lognormal_from_moments.
Likelihood
obs ~ noise(f_plume(emission_rate, ...) + background)
with noise defaulting to GaussianNoise(obs_noise_std), i.e.
obs ~ Normal(f_plume + background, σ_obs).
Parameters¶
observations : jnp.ndarray, shape (N,), optional
Observed concentrations [kg/m³]. If None, runs in prior-predictive mode.
receptor_coords : tuple of jnp.ndarray, optional
(x, y, z), each shape (N,), in the fixed frame [m]. Required
whenever observations is not None.
source_location : tuple of float, optional
(x, y, z) source coordinates [m]. Required whenever observations
is not None.
wind_u, wind_v : float, optional
Wind velocity components [m/s]. Required whenever observations
is not None.
stability_class : str
Stability class used if infer_stability=False. Default 'C'.
prior_emission_rate_mean, prior_emission_rate_std : float
Real-scale prior mean / std for the emission rate [kg/s]. The
implementation converts these to log-space parameters internally, so
the realised prior has the requested mean and variance.
infer_stability : bool
If True, sample a categorical stability_idx latent over A-F.
background_prior_std : float
HalfNormal scale for the background concentration [kg/m³].
obs_noise_std : float
Observation noise σ [kg/m³] of the default Gaussian noise. Ignored
when noise is given.
noise : Noise, optional
Observation-noise model, e.g. StudentTNoise(scale, df=5.0) or
HeteroscedasticNoise(std) from :mod:plumax.gauss_plume.likelihoods.
Default None is GaussianNoise(obs_noise_std).
Raises¶
ValueError
If observations is provided but any of receptor_coords,
source_location, wind_u, or wind_v is None — that
combination disconnects the likelihood from the forward model and
would produce a meaningless posterior. Prior-predictive runs
(observations=None) are allowed with any subset of forward-model
inputs.
Source code in src/plumax/gauss_plume/inference.py
107 108 109 110 111 112 113 114 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 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 | |
infer_emission_rate
¶
infer_emission_rate(observations: NDArray, observation_coords: tuple[NDArray, NDArray, NDArray], source_location: tuple[float, float, float], wind_speed: float, wind_direction: float, stability_class: str = 'C', infer_stability: bool = False, prior_mean: float = 0.1, prior_std: float = 0.05, num_warmup: int = 500, num_samples: int = 1000, num_chains: int = 1, seed: int = 0, progress_bar: bool = False, print_summary: bool = False, obs_noise_std: float = 5e-07, background_prior_std: float = 5e-07, noise: Noise | None = None) -> EmissionRateSamples
Infer the plume emission rate via NUTS.
Wraps :func:gaussian_plume_model with input validation, wind-from-angle
to (u, v) conversion, and posterior unpacking into NumPy arrays.
Identifiability target: three overpasses with varied wind direction should give a posterior CV(Q) ≤ 50 %, and a single overpass CV(Q) ≤ 100 % under a tight met wind prior (Tier I design, "Identifiability — quantitative target"). A wider posterior means the data do not constrain Q yet.
Parameters¶
observations : ndarray, shape (N,)
Observed concentrations [kg/m³].
observation_coords : tuple of ndarray
(x, y, z), each shape (N,), in the fixed frame [m].
source_location : tuple of float, (3,)
(x, y, z) source coordinates [m].
wind_speed : float
Wind speed magnitude [m/s]. Must be > 0.
wind_direction : float
Wind direction [degrees from North, meteorological "from" convention].
stability_class : str
Stability class used if infer_stability=False. Default 'C'.
infer_stability : bool
If True, jointly sample a categorical stability-class latent.
prior_mean, prior_std : float
Prior parameters for the emission rate [kg/s].
num_warmup, num_samples, num_chains : int
NUTS sampler configuration.
seed : int
PRNG seed.
progress_bar : bool
If True, show the NumPyro progress bar. Default False.
print_summary : bool
If True, call mcmc.print_summary() after the run.
obs_noise_std : float
Observation-noise σ for the Gaussian likelihood [kg/m³]. Set this to
the sensor noise so the posterior width reflects the instrument rather
than the hard-coded default. Ignored when noise is given.
background_prior_std : float
HalfNormal scale for the background-concentration prior [kg/m³].
noise : Noise, optional
Observation-noise model (see :func:gaussian_plume_model). Default
None is GaussianNoise(obs_noise_std), today's likelihood.
Returns¶
samples : EmissionRateSamples
Posterior draws keyed by site name: 'emission_rate',
'background', and (if infer_stability) 'stability_idx'.
A dict that also carries the Tier V per-event payload
(emission_rate / emission_std attributes, samples and
prior_logpdf).
Raises¶
ValueError
On invalid stability class, non-positive wind speed, shape mismatches
between observations and observation_coords, or empty data.
Source code in src/plumax/gauss_plume/inference.py
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 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 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 | |
infer_multi_source
¶
infer_multi_source(observations: NDArray, observation_coords: tuple[NDArray, NDArray, NDArray], *, wind_speed: float, wind_direction: float, k_max: int = 10, candidate_locations: NDArray | None = None, source_bounds: tuple[tuple[float, float], tuple[float, float]] | None = None, source_height: float = 0.0, stability_class: str = 'C', prior_mean: float = 0.1, prior_std: float = 0.05, prior_active: float = 0.3, obs_noise_std: float = 5e-07, background_prior_std: float = 5e-07, noise: Noise | None = None, num_warmup: int = 500, num_samples: int = 1000, num_chains: int = 1, seed: int = 0, progress_bar: bool = False) -> MultiSourcePosterior
Infer up to K_max sources and their on/off indicators.
Wraps :func:multi_source_plume_model: NUTS for the continuous sites, and
a Gibbs step (DiscreteHMCGibbs) over the activity indicators, as for
the stability class in :func:infer_emission_rate.
Parameters¶
observations : ndarray, shape (N,)
Observed concentrations [kg/m³].
observation_coords : tuple of ndarray
(x, y, z), each shape (N,), in the fixed frame [m].
wind_speed : float
Wind speed [m/s], > 0.
wind_direction : float
Wind direction [degrees from North, meteorological "from" convention].
k_max, candidate_locations, source_bounds, source_height, stability_class
Slot layout; see :func:multi_source_plume_model.
prior_mean, prior_std : float
Real-scale prior moments of each Q_k [kg/s].
prior_active, obs_noise_std, background_prior_std, noise
See :func:multi_source_plume_model.
num_warmup, num_samples, num_chains, seed : int
Sampler configuration.
progress_bar : bool
Show NumPyro's progress bar.
Returns¶
posterior : MultiSourcePosterior
Draws of every slot; count_probabilities() is the posterior over
the active count and per_source(k) the slot's summary.
Raises¶
ValueError On a non-positive wind speed, an unknown stability class, mismatched shapes, non-positive prior or noise scales, or an invalid slot layout.
Source code in src/plumax/gauss_plume/inference.py
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 | |
multi_source_plume_model
¶
multi_source_plume_model(observations: ndarray | None = None, *, receptor_coords: tuple[ndarray, ndarray, ndarray], wind_u: float, wind_v: float, k_max: int = 10, candidate_locations: ndarray | None = None, source_bounds: tuple[tuple[float, float], tuple[float, float]] | None = None, source_height: float = 0.0, stability_class: str = 'C', prior_emission_rate_mean: float = 0.1, prior_emission_rate_std: float = 0.05, prior_active: float = 0.3, background_prior_std: float = 5e-07, obs_noise_std: float = 5e-07, noise: Noise | None = None) -> jnp.ndarray
Masked-K NumPyro model: up to K_max plumes, each switched on or off.
Priors, for each slot k = 1 … K_max:
a_k ~ Bernoulli(prior_active) [on/off]
Q_k ~ LogNormal(μ_log, σ_log) [kg/s]
(x_k, y_k) = candidate k, or x ordered uniform and y uniform in
source_bounds [m]
background ~ HalfNormal(σ_bg) [kg/m³]
Likelihood
obs ~ noise(Σ_k a_k Q_k c₁(x_k, y_k) + background)
with noise defaulting to GaussianNoise(obs_noise_std). The active
count K = Σ_k a_k is the posterior over the number of sources.
Label switching: with free locations the slots are exchangeable, so their
x positions are drawn as the order statistics of K_max uniforms on
[x_low, x_high] (x_low + (x_high − x_low) · cumsum(w)[:K_max] with
w ~ Dirichlet(1, …, 1) of length K_max + 1), which pins slot k to the
k-th source from the west. Fixed candidate locations (known facilities)
have no symmetry to break.
Promotion to RJMCMC (Tier I / IV design): masked-K caps K at K_max. Track
the posterior mass at the cap, P(K = K_max | y)
(:meth:MultiSourcePosterior.saturated_fraction), per overpass; when more
than 5 % of overpasses saturate K_max, the cap is binding and the
trans-dimensional (RJMCMC) model is due.
Parameters¶
observations : jnp.ndarray, shape (N,), optional
Observed concentrations [kg/m³]; None for predictive draws.
receptor_coords : tuple of jnp.ndarray
(x, y, z), each shape (N,), in the fixed frame [m].
wind_u, wind_v : float
Wind velocity components [m/s].
k_max : int
Number of slots K_max (default 10, the design's v1 cap).
candidate_locations : jnp.ndarray, shape (k_max, 2), optional
Fixed (x, y) per slot, e.g. inventory facilities.
source_bounds : ((x_low, x_high), (y_low, y_high)), optional
Box the free slot locations are drawn from. Give exactly one of this
and candidate_locations.
source_height : float
Release height shared by every slot [m].
stability_class : str
Pasquill–Gifford class 'A' … 'F'.
prior_emission_rate_mean, prior_emission_rate_std : float
Real-scale prior moments of each Q_k [kg/s].
prior_active : float
Prior probability that a slot is on, in (0, 1]. 1.0 keeps every
slot on (no discrete sites), which with K_max = 1 is the single-source
model.
background_prior_std : float
HalfNormal scale of the background [kg/m³].
obs_noise_std : float
σ of the default Gaussian noise; ignored when noise is given.
noise : Noise, optional
Observation-noise model from :mod:plumax.gauss_plume.likelihoods.
Raises¶
ValueError
On an invalid slot layout, k_max < 1 or prior_active outside
(0, 1].
Source code in src/plumax/gauss_plume/inference.py
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 | |