Lagrangian particles¶
Lagrangian particle dispersion. Tier II.
lagrangian
¶
Tier II — Lagrangian particle dispersion (JAX).
A Markov-1 Langevin particle model: stochastic trajectories driven by mean wind plus an Ornstein–Uhlenbeck turbulent-velocity process. The bridge between the analytical Tier I (no real wind variability) and the Eulerian Tier III. See the Tier II design doc.
This implements Step 1 (the forward model) and Step 2 (model-based inference):
turbulence—HomogeneousTurbulence(well-mixed-exact) and Hanna (1982) Monin–Obukhov-similarity profiles.particles— the Markov-1 integrator with the Thomson (1987) well-mixed vertical drift correction and ground / PBL reflection.concentration— forward steady concentration field via residence-time binning (simulate_lagrangian).footprint— backward source–receptor sensitivity (compute_footprint).inversion— closed-form Gaussian and sign-constrained lognormal Bayesian inversion of the source vector from observations, with a Matérn-3/2 spatial prior (linear_gaussian_inversion/lognormal_inversion).
The footprint emulator and the amortized predictor (Steps 3–5) are not yet implemented.
GaussianPosterior
dataclass
¶
Closed-form Gaussian posterior over the source vector.
Attributes:
| Name | Type | Description |
|---|---|---|
mean |
Array
|
Posterior mean emission field |
covariance |
Array
|
Posterior covariance |
LognormalPosterior
dataclass
¶
Linearised lognormal posterior over a non-negative source vector.
Attributes:
| Name | Type | Description |
|---|---|---|
mean |
Array
|
Posterior mean emission field |
log_increment |
Array
|
Log-space MAP increment |
log_covariance |
Array
|
Posterior covariance in log-space |
ParticleState
dataclass
¶
State of an ensemble of Lagrangian particles.
Registered as a JAX pytree so it can be carried through lax.scan /
vmap; position and velocity are the (array) children.
Attributes:
| Name | Type | Description |
|---|---|---|
position |
Array
|
Particle positions |
velocity |
Array
|
Turbulent velocity perturbations [m/s], shape |
HannaTurbulence
dataclass
¶
HannaTurbulence(u_star: float, pbl_height: float, obukhov_length: float, w_star: float = 0.0, z_floor: float = 0.1)
Height-dependent Hanna (1982) turbulence as a TurbulenceModel.
Thin adapter that binds the surface-layer scales required by
:func:hanna_profiles and exposes the at(z) -> (sigma, tau) interface
the integrator (:func:plumax.lagrangian.langevin_step,
:func:~plumax.lagrangian.integrate_particles,
:func:~plumax.lagrangian.compute_footprint) expects. Because σ_w varies
with height, the well-mixed vertical drift correction ½(1+v_w²/σ_w²)
∂σ_w²/∂z is non-zero — the inhomogeneous path that
:class:HomogeneousTurbulence never exercises.
Attributes:
| Name | Type | Description |
|---|---|---|
u_star |
float
|
Friction velocity |
pbl_height |
float
|
Boundary-layer height |
obukhov_length |
float
|
Monin–Obukhov length |
w_star |
float
|
Convective velocity scale |
z_floor |
float
|
Minimum height [m] at which the profiles are evaluated. The
convective |
at
¶
Return (σ, τ_L) at height(s) z from the Hanna (1982) profiles.
z is floored at :attr:z_floor before evaluation so the vertical
drift stays finite at the ground (see the class docstring). σ and
τ_L have a trailing axis of size 3, matching
:meth:HomogeneousTurbulence.at.
Source code in src/plumax/lagrangian/turbulence.py
HomogeneousTurbulence
dataclass
¶
HomogeneousTurbulence(sigma_u: float, sigma_v: float, sigma_w: float, tau_u: float, tau_v: float, tau_w: float)
Spatially uniform, well-mixed-exact turbulence statistics.
Attributes:
| Name | Type | Description |
|---|---|---|
sigma_u |
float
|
Along-wind velocity standard deviation [m/s]. |
sigma_v |
float
|
Cross-wind velocity standard deviation [m/s]. |
sigma_w |
float
|
Vertical velocity standard deviation [m/s]. |
tau_u |
float
|
Along-wind Lagrangian timescale [s]. |
tau_v |
float
|
Cross-wind Lagrangian timescale [s]. |
tau_w |
float
|
Vertical Lagrangian timescale [s]. |
isotropic
classmethod
¶
Build isotropic turbulence: equal σ and τ_L on all axes.
at
¶
Return (σ, τ_L) at height(s) z — constant, so z is ignored.
The signature matches :func:hanna_profiles so the integrator can treat
any turbulence model uniformly. σ and τ_L broadcast to the shape
of z along a trailing axis of size 3.
Source code in src/plumax/lagrangian/turbulence.py
bin_positions
¶
Histogram particle positions (n, 3) onto a 3-D grid (counts).
Source code in src/plumax/lagrangian/concentration.py
simulate_lagrangian
¶
simulate_lagrangian(emission_rate: float, source_location: tuple[float, float, float], turbulence: TurbulenceModel, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], domain_z: tuple[float, float, int], *, n_particles: int = 5000, t_end: float = 600.0, dt: float = 1.0, wind: Callable[[Array], Array] | None = None, wind_speed: float | None = None, wind_direction: float | None = None, pbl_height: float | None = None, background_conc: float = 0.0, seed: int = 0) -> xr.Dataset
Simulate a steady Lagrangian concentration field on a 3-D grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
emission_rate
|
float
|
Continuous source strength |
required |
source_location
|
tuple[float, float, float]
|
|
required |
turbulence
|
TurbulenceModel
|
Turbulence model (e.g.
:class: |
required |
domain_x
|
tuple[float, float, int]
|
|
required |
domain_y
|
tuple[float, float, int]
|
|
required |
domain_z
|
tuple[float, float, int]
|
|
required |
n_particles
|
int
|
Number of particles in the ensemble. |
5000
|
t_end
|
float
|
Integration horizon [s]. |
600.0
|
dt
|
float
|
Time step [s]. |
1.0
|
wind
|
Callable[[Array], Array] | None
|
Mean-wind field |
None
|
wind_speed
|
float | None
|
Wind speed [m/s] (with |
None
|
wind_direction
|
float | None
|
Meteorological direction [deg from North]. |
None
|
pbl_height
|
float | None
|
Optional reflecting PBL lid [m]. |
None
|
background_conc
|
float
|
Additive background concentration [kg/m³]. |
0.0
|
seed
|
int
|
PRNG seed. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
Dataset
|
class: |
Dataset
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
on a non-positive emission rate, a negative source height, or an ambiguous / missing wind specification. |
Source code in src/plumax/lagrangian/concentration.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 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 184 185 186 187 188 189 190 | |
compute_footprint
¶
compute_footprint(receptor_location: tuple[float, float, float], turbulence: TurbulenceModel, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], *, wind: Callable[[Array], Array], n_particles: int = 5000, t_back: float = 600.0, dt: float = 1.0, pbl_height: float = 1000.0, pbl_fraction: float = 0.5, air_density: float = 1.2, receptor_time: float = 0.0, seed: int = 0) -> tuple[np.ndarray, np.ndarray, np.ndarray]
Compute a single receptor's surface footprint by backward integration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receptor_location
|
tuple[float, float, float]
|
|
required |
turbulence
|
TurbulenceModel
|
Turbulence model. |
required |
domain_x
|
tuple[float, float, int]
|
|
required |
domain_y
|
tuple[float, float, int]
|
|
required |
wind
|
Callable[[Array], Array]
|
Forward mean-wind field |
required |
n_particles
|
int
|
Ensemble size. |
5000
|
t_back
|
float
|
Backward integration time [s]. |
600.0
|
dt
|
float
|
Time step [s]. |
1.0
|
pbl_height
|
float
|
Boundary-layer height |
1000.0
|
pbl_fraction
|
float
|
Fraction |
0.5
|
air_density
|
float
|
Air density |
1.2
|
receptor_time
|
float
|
Physical time |
0.0
|
seed
|
int
|
PRNG seed. |
0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
ndarray
|
|
ndarray
|
docstring). |
Source code in src/plumax/lagrangian/footprint.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 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 | |
linear_gaussian_inversion
¶
linear_gaussian_inversion(footprint: Array, observation: Array, *, prior_mean: Array, prior_cov: Array, obs_variance: Array) -> GaussianPosterior
Closed-form Gaussian–Gaussian (BLUE) inversion of the source vector.
For a Gaussian prior q ~ N(q_a, B) and Gaussian likelihood
y = F q + ε, ε ~ N(0, R) (R diagonal), returns the exact
posterior N(q*, P*) with::
q* = q_a + B Fᵀ (F B Fᵀ + R)⁻¹ (y − F q_a)
P* = B − B Fᵀ (F B Fᵀ + R)⁻¹ F B
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Array
|
Source–receptor matrix |
required |
observation
|
Array
|
Observed values |
required |
prior_mean
|
Array
|
Prior mean |
required |
prior_cov
|
Array
|
Prior covariance |
required |
obs_variance
|
Array
|
Diagonal of |
required |
Returns:
| Name | Type | Description |
|---|---|---|
The |
GaussianPosterior
|
class: |
Source code in src/plumax/lagrangian/inversion.py
lognormal_inversion
¶
lognormal_inversion(footprint: Array, observation: Array, *, prior_mean: Array, prior_log_cov: Array, obs_variance: Array) -> LognormalPosterior
Sign-constrained inversion via a lognormal prior linearised in log-space.
With log q ~ N(log q_a, B_log) and the forward linearised about the
prior mean, the tangent-linear Jacobian is F̃ = F · diag(q_a) and the
log-space increment δ = log(q / q_a) follows the same BLUE update::
δ* = B_log F̃ᵀ (F̃ B_log F̃ᵀ + R)⁻¹ (y − F q_a)
q* = q_a · exp(δ*) (≥ 0 by construction)
This is the design note's recommended v1 prior: it enforces non-negative
emissions while keeping the update closed-form (no projected-gradient or
NNLS inner loop). The innovation y − F q_a is the observation minus the
forward evaluated at the prior mean (δ = 0 ⇒ q = q_a).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
footprint
|
Array
|
Source–receptor matrix |
required |
observation
|
Array
|
Observed values |
required |
prior_mean
|
Array
|
Prior median emission field |
required |
prior_log_cov
|
Array
|
Log-space prior covariance |
required |
obs_variance
|
Array
|
Diagonal of |
required |
Returns:
| Name | Type | Description |
|---|---|---|
The |
LognormalPosterior
|
class: |
Source code in src/plumax/lagrangian/inversion.py
matern32_covariance
¶
Build a Matérn-3/2 prior covariance over grid-cell coordinates.
The Matérn-3/2 kernel
k(r) = σ² (1 + √3 r / ℓ) exp(−√3 r / ℓ), r = ‖xᵢ − xⱼ‖
gives a once-differentiable random field — smoother than the exponential
(AR(1) / Matérn-1/2) kernel but far less rigid than a squared-exponential.
It is the design note's recommended spatial prior for regional emission
inversions: a real correlation length ℓ regularises the posterior so
unresolved structure spreads over neighbouring cells instead of collapsing
into single-cell spikes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coordinates
|
Array
|
Cell-centre coordinates [m], shape |
required |
variance
|
float
|
Marginal prior variance |
required |
length_scale
|
float
|
Correlation length |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The |
Source code in src/plumax/lagrangian/inversion.py
observation_covariance
¶
observation_covariance(retrieval_variance: Array, *, representation_variance: float | Array = 0.0) -> jax.Array
Assemble the diagonal observation-error covariance R.
R = R_retr + R_repr: the per-pixel L2 retrieval error plus a
representation error that accounts for model-vs-observation footprint
mismatch. The design note is explicit that omitting R_repr (using the
retrieval error alone) overweights the observations and yields overconfident
posteriors, so the representation term is a first-class argument here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
retrieval_variance
|
Array
|
Per-observation retrieval variance |
required |
representation_variance
|
float | Array
|
Representation variance |
0.0
|
Returns:
| Type | Description |
|---|---|
Array
|
The diagonal of |
Source code in src/plumax/lagrangian/inversion.py
integrate_particles
¶
integrate_particles(state: ParticleState, wind: Callable[[Array], Array], turbulence: TurbulenceModel, *, t0: float, t1: float, dt: float, key: Array, pbl_height: float | None = None, save_trajectory: bool = False) -> tuple[ParticleState, jax.Array | None]
Integrate the ensemble from t0 to t1 with fixed step dt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
ParticleState
|
Initial particle state. |
required |
wind
|
Callable[[Array], Array]
|
Mean-wind field |
required |
turbulence
|
TurbulenceModel
|
Turbulence model. |
required |
t0
|
float
|
Start time [s]. |
required |
t1
|
float
|
End time [s]. |
required |
dt
|
float
|
Time step [s]. |
required |
key
|
Array
|
PRNG key. |
required |
pbl_height
|
float | None
|
Optional PBL-top reflecting lid [m]. |
None
|
save_trajectory
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
ParticleState
|
|
Array | None
|
|
Source code in src/plumax/lagrangian/particles.py
langevin_step
¶
langevin_step(state: ParticleState, wind: Array, turbulence: TurbulenceModel, dt: float, key: Array, *, pbl_height: float | None = None) -> ParticleState
Advance the ensemble one step dt with the Markov-1 update.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
ParticleState
|
Current particle state. |
required |
wind
|
Array
|
Mean wind |
required |
turbulence
|
TurbulenceModel
|
Turbulence model supplying |
required |
dt
|
float
|
Time step [s]. |
required |
key
|
Array
|
PRNG key for the Wiener increment. |
required |
pbl_height
|
float | None
|
If given, reflect particles off the PBL top [m]. |
None
|
Returns:
| Type | Description |
|---|---|
ParticleState
|
The advanced :class: |
Source code in src/plumax/lagrangian/particles.py
n_steps_for_horizon
¶
Number of fixed-dt steps covering horizon (ceil, float-robust).
Plain ceil(horizon / dt) adds a spurious extra step when the quotient is
mathematically an integer but floating-point rounds it just above (e.g.
2.1 / 0.15 → 14.000…02). Snapping the quotient to the nearest integer
when it is within rtol avoids that degenerate final zero-length step.
Returns 0 for a non-positive horizon.
Source code in src/plumax/lagrangian/particles.py
step_durations
¶
Per-step durations summing to horizon; the final step takes the rest.
All steps are dt except the last, which is the remainder
horizon - (n_steps - 1) * dt (equal to dt when horizon is an
exact multiple). Returns an empty array when n_steps == 0. Pair with
:func:n_steps_for_horizon so the final duration is always > 0 (no
degenerate zero-length step from float round-up). Used by the integrator /
residence / footprint paths so a non-divisible horizon advances over exactly
horizon rather than n_steps * dt.
Source code in src/plumax/lagrangian/particles.py
uniform_wind
¶
Return a constant mean-wind field t -> (u, v, w).
Source code in src/plumax/lagrangian/particles.py
wind_from_speed_direction
¶
wind_from_speed_direction(speed: float, direction_deg: float, w: float = 0.0) -> Callable[[jax.Array], jax.Array]
Constant wind from speed + meteorological direction (degrees from North).
direction_deg = 270 is a wind from the west (flowing east), matching the
convention used by :func:plumax.gauss_plume.simulate_plume.
Source code in src/plumax/lagrangian/particles.py
hanna_profiles
¶
hanna_profiles(z: Array, *, u_star: float, pbl_height: float, obukhov_length: float, w_star: float = 0.0) -> tuple[jax.Array, jax.Array]
Hanna (1982) Monin–Obukhov-similarity turbulence profiles.
Returns height-dependent velocity standard deviations and Lagrangian
timescales for the three stability regimes selected by the sign of the
Obukhov length L:
- Unstable / convective (
L < 0): convective scaling withw_*. - Stable (
L > 0): surface-layer scaling decaying withz / h. - Neutral (
|L|large):σ ∝ u_*with an exponential PBL decay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Array
|
Height(s) above ground [m]; any shape. |
required |
u_star
|
float
|
Friction velocity [m/s]. |
required |
pbl_height
|
float
|
Boundary-layer height |
required |
obukhov_length
|
float
|
Monin–Obukhov length |
required |
w_star
|
float
|
Convective velocity scale [m/s] (used when |
0.0
|
Returns:
| Type | Description |
|---|---|
Array
|
|
Array
|
standard deviations [m/s] and Lagrangian timescales [s] for |
tuple[Array, Array]
|
|
Source code in src/plumax/lagrangian/turbulence.py
95 96 97 98 99 100 101 102 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 | |