Eulerian finite volume¶
Eulerian 3-D advection–diffusion on an Arakawa C-grid via finitevolX. Tier III.
les_fvm
¶
Eulerian 3-D advection-diffusion of a methane tracer (Level-2 fidelity).
This sub-package implements a finite-volume solver for the passive-tracer transport equation
∂C/∂t + ∇·(u C) = ∇·(K ∇C) + S(x, t)
on an Arakawa C-grid (via finitevolX), with a prescribed wind field
u = (u, v, w) and K-theory (possibly anisotropic) eddy diffusivity
K = diag(K_h, K_h, K_z). No turbulence is resolved — this model sits
between the analytic Gaussian puff (L1) and a full resolved-flow LES (L3).
Submodules¶
grid: CartesianGrid3D wrapper + coordinate helpers.wind: prescribed 3-D wind fields (uniform, sheared, time-varying).source: Gaussian point-source and per-cell surface-flux terms.advection: horizontal WENO advection (finitevolX) + vertical upwind.diffusion: K-theory eddy diffusivity + anisotropic Laplacian.boundary: BC application for 3-D fields (horizontal vmap + vertical).dynamics: diffrax-compatible RHS for the scalar transport equation.simulate: high-level xarray-returning runner._vertical_ops: private — vertical ∂_z / flux / Laplacian helpers.
HorizontalBC
¶
Bases: Module
Apply a :class:BoundaryConditionSet to every z-slice of a 3-D field.
advection_variant
¶
Return a copy whose Dirichlet faces use a cell-value ghost.
Open-mode advection reads the ghost as an exterior cell value; a
face-Dirichlet ghost (2*value - interior) would make an inflow
carry the wrong state. Outflow / periodic / Neumann faces are
unchanged, so this is a no-op unless a Dirichlet face is present.
Source code in src/plumax/les_fvm/boundary.py
VerticalBC
¶
Bases: Module
Top/bottom ghost-slice update for a 3-D field.
Parameters¶
bottom_kind : {"dirichlet", "neumann", "outflow", "periodic"}
Ground-boundary behaviour.
bottom_value : float, default 0.0
For dirichlet: the boundary value.
For neumann: the coordinate gradient ∂C/∂z along +z
at the face — a positive value means C increases with height.
The ghost cell is set so the finite-difference ∂C/∂z across
the face equals value. Ignored for outflow / periodic.
top_kind, top_value : same, with the Neumann value again the
coordinate gradient ∂C/∂z along +z (same convention on
both faces; see the sign note in :func:_apply_vertical_face).
advection_variant
¶
Return a copy whose Dirichlet faces use a cell-value ghost.
The vertical advection term also upwinds the ghost slice, so a Dirichlet vertical inflow needs the same cell-value convention as the horizontal faces. A no-op unless a vertical Dirichlet face is set.
Source code in src/plumax/les_fvm/boundary.py
EddyDiffusivity
¶
Bases: Module
Scalar or field-valued (K_h, K_z) eddy diffusivity.
Attributes¶
horizontal : float or Float[Array, "nz ny nx"] Horizontal eddy diffusivity [m²/s]. A scalar is broadcast over the grid. A field is expected on interior T-points. vertical : float or Float[Array, "nz ny nx"] Vertical eddy diffusivity [m²/s]. Same broadcasting rules.
as_arrays
¶
Return (K_h, K_z) in forms consumable by the tendency calls.
Source code in src/plumax/les_fvm/diffusion.py
EulerianDispersionRHS
¶
Bases: Module
Full tracer-transport tendency for diffrax.
Parameters¶
plume_grid : PlumeGrid3D
wind_field : PrescribedWindField
Time-queryable prescribed wind.
eddy_diffusivity : EddyDiffusivity
(K_h, K_z) eddy diffusivity.
source : GaussianSource or SurfaceFluxSource
Methane source term.
horizontal_bc : HorizontalBC
vertical_bc : VerticalBC
advection_scheme : str, default "weno5"
Horizontal reconstruction scheme for
:func:advection.advection_tendency.
BackgroundAnalysis
dataclass
¶
The MAP of the :class:BackgroundControl part of a 4D-Var control.
Attributes:
| Name | Type | Description |
|---|---|---|
initial_concentration |
Array
|
c₀ = c_b + δc₀ on the interior [kg m⁻³], shape
|
bc_scale |
Array
|
Per-face factors α*, ordered (west, east, south, north). |
BackgroundControl
dataclass
¶
BackgroundControl(c_b: Array, B_c: AbstractLinearOperator, bc_scale_prior: tuple[float, float] = (1.0, 0.1))
Initial concentration and per-face inflow scaling in the control.
The flat control becomes (S, δc₀, α), with priors independent of the source's,
c₀ = c_b + δc₀, δc₀ ~ N(0, B_c), α_f ~ N(α̅, σ_α²) for each face f,
and every Dirichlet lateral boundary value c_f of the forward becomes
α_f c_f (:func:~plumax.les_fvm.boundary.scale_dirichlet_faces). The factors
are ordered (west, east, south, north). This is the per-face Gaussian of the
design page; a per-face Matérn along the boundary is its open v2.
Attributes:
| Name | Type | Description |
|---|---|---|
c_b |
Array
|
Background initial concentration c_b on the interior cells [kg m⁻³],
shape |
B_c |
AbstractLinearOperator
|
Covariance of δc₀, a lineax operator on the flattened interior. |
bc_scale_prior |
tuple[float, float]
|
Mean α̅ and standard deviation σ_α of each factor. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
A background on a 2 × 3 × 4 interior adds 24 + 4 entries to the control:
>>> import jax
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> covariance = lx.DiagonalLinearOperator(jnp.ones(24))
>>> background = BackgroundControl(jnp.zeros((2, 3, 4)), covariance)
>>> background.size
28
split
¶
Split a flat control into (S, δc₀ as a field, α).
Source code in src/plumax/les_fvm/fourdvar.py
flat_prior_mean
¶
Prior mean of the (δc₀, α) part, (0, α̅).
Source code in src/plumax/les_fvm/fourdvar.py
ColumnObservationOperator
dataclass
¶
Sample the column-integrated field at fixed receptor cells.
H_t in the 4D-Var cost: vertically integrate the 3-D concentration to a
column (∑_z c · dz), then gather the values at a set of receptor pixels.
With receptor_index = None every interior column is an observation.
A thin wrapper over the shared
:class:~plumax.gauss_plume.observation.ColumnObservation (identity
averaging kernel, h_k = dz), whose module docstring states the units.
Attributes:
| Name | Type | Description |
|---|---|---|
dz |
float
|
Vertical cell size [m] for the column integral. |
receptor_index |
Array | None
|
Optional |
EulerianForward4DVar
dataclass
¶
EulerianForward4DVar(rhs_factory: Callable[..., EulerianDispersionRHS], observation_op: ColumnObservationOperator, save_times: Array, initial_concentration: Array, t0: float, t1: float, solver_kwargs: dict = dict(), plume_grid: PlumeGrid3D | None = None, control: SpatialControl | None = None, background: BackgroundControl | None = None)
Differentiable map from an emission time series to column observations.
Bundles the fixed transport configuration (grid, wind, diffusivity, BCs,
source geometry, save times) so that :meth:predict is a pure function of
the control vector q — the time-resolved emission rate at the save
times, or the flat per-cell flux of a :class:SpatialControl — and is
therefore jax.grad / jax.linearize-able for 4D-Var.
Build with :func:build_forward rather than directly.
Attributes:
| Name | Type | Description |
|---|---|---|
rhs_factory |
Callable[..., EulerianDispersionRHS]
|
|
observation_op |
ColumnObservationOperator
|
The column |
save_times |
Array
|
Observation/integration times [s], shape |
initial_concentration |
Array
|
Padded initial field |
solver_kwargs |
dict
|
diffrax solve settings (solver, dt0, tolerances). |
plume_grid |
PlumeGrid3D | None
|
The analysis grid; its T-cells place a spatial prior. |
control |
SpatialControl | None
|
The spatial control the flat control vector is laid out by;
|
background |
BackgroundControl | None
|
The initial-concentration and boundary-scaling part of the
flat control; |
with_background
¶
This forward over the flat control (S, δc₀, α) of background.
concentration_history
¶
Solve the FV transport for q_series; return interior fields.
Returns the (n_t, nz, ny, nx) interior concentration history (ghost
rings stripped). Differentiable in q_series via the diffrax solve.
Source code in src/plumax/les_fvm/fourdvar.py
predict
¶
Column observations stacked over time, shape (n_t, n_obs).
FourDVarProblem
dataclass
¶
FourDVarProblem(forward: EulerianForward4DVar, prior_mean: Array, prior_chol: Array, observations: Array, obs_variance: Array, control: SpatialControl | None = None, prior_sqrt_op: AbstractLinearOperator | None = None)
A strong-constraint 4D-Var problem over a flat source control.
The control is a scalar emission time series or the per-cell flux of a
:class:SpatialControl, optionally stacked with a :class:BackgroundControl;
n_state below is its length. Holds the differentiable forward, the prior (S_b, B = L Lᵀ),
and the observations (y_t, R_t). The cost is evaluated in whitened space
χ = L⁻¹ (S − S_b) so the prior term is the identity ½‖χ‖² and the
Hessian is well-conditioned (design §control-transform). observations
has shape (n_t, n_obs) and obs_variance broadcasts to it.
Build with :func:build_problem.
prior_sqrt_operator
¶
The prior square root L as an operator, structured where it can be.
Source code in src/plumax/les_fvm/fourdvar.py
source_from_whitened
¶
cost
¶
The 4D-Var cost J at whitened control χ (scalar).
A diagnostic: the solve minimises the same cost inside
vardax.ThreeDVar, not through this method.
Source code in src/plumax/les_fvm/fourdvar.py
value_and_grad
¶
(J, ∂J/∂χ) — the gradient is the exact adjoint via reverse-mode AD.
source_receptor_operator
¶
The whitened source–receptor map as a vardax observation operator.
𝒢(S) = vec(forward.predict(S)) / σ on a (1, n_t) source row,
so the observation-error covariance is the identity. The diffrax
solve defines only a VJP, whose while_loop JAX cannot transpose,
so the operator linearises by forming the Jacobian with one VJP per
observation (jac="bwd", materialise=True), so the cost scales with
the number of observations rather than the length of the control.
Returns:
| Type | Description |
|---|---|
CallableObs
|
A |
Raises:
| Type | Description |
|---|---|
ImportError
|
Without the |
Source code in src/plumax/les_fvm/fourdvar.py
incremental_analysis
¶
incremental_analysis(*, outer_iters: int = 5, inner_iters: int = 50, rtol: float = 1e-06, atol: float = 1e-08) -> tuple[vardax.IncrementalFourDVar, vardax.Batch1D]
The Gauss–Newton analysis of this problem, and its one-window batch.
vardax.IncrementalFourDVar with an identity forward over a
one-time window, because the control is a source rather than an initial
state, over the whitened source–receptor map with R = I. Each outer
iteration forms the Jacobian with one VJP per observation
(jac="bwd") and solves I + (J̃ L)ᵀ (J̃ L) in the control-variable
transform by CG.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
outer_iters
|
int
|
Gauss–Newton outer iterations. |
5
|
inner_iters
|
int
|
CG iterations per outer iteration. |
50
|
rtol
|
float
|
Relative tolerance of the CG solves. |
1e-06
|
atol
|
float
|
Absolute tolerance of the CG solves. |
1e-08
|
Returns:
| Type | Description |
|---|---|
IncrementalFourDVar
|
|
Batch1D
|
|
Raises:
| Type | Description |
|---|---|
ImportError
|
Without the |
Source code in src/plumax/les_fvm/fourdvar.py
analysis
¶
analysis(*, minimiser: AbstractMinimiser | None = None, max_steps: int = 100, initial_whitened: Array | None = None) -> tuple[vardax.ThreeDVar, vardax.Batch1D]
The 3D-Var analysis of this problem, and its one-window batch.
vardax.ThreeDVar over :meth:source_receptor_operator, with the
whitened control S = S_b + L χ, ½‖χ‖², as a
vardax.LinearParameterization and R = I on the whitened
observations y / σ.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
minimiser
|
AbstractMinimiser | None
|
An optimistix minimiser. Default
|
None
|
max_steps
|
int
|
Iteration cap. |
100
|
initial_whitened
|
Array | None
|
First guess |
None
|
Returns:
| Type | Description |
|---|---|
ThreeDVar
|
|
Batch1D
|
|
tuple[ThreeDVar, Batch1D]
|
diagnostics, and |
tuple[ThreeDVar, Batch1D]
|
posterior. |
Raises:
| Type | Description |
|---|---|
ImportError
|
Without the |
Source code in src/plumax/les_fvm/fourdvar.py
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 | |
FourDVarResult
dataclass
¶
FourDVarResult(source: Array, whitened: Array, cost: float, n_steps: int, posterior: PosteriorCovariance | None = None, converged: bool = True, prior_mean: Array | None = None, prior_covariance: Array | None = None, control: SpatialControl | None = None, background: BackgroundAnalysis | None = None)
Outcome of a 4D-Var minimisation.
Attributes:
| Name | Type | Description |
|---|---|---|
source |
Array
|
MAP emission time series |
whitened |
Array
|
Optimal whitened control |
cost |
float
|
Final cost value |
n_steps |
int
|
Optimiser iterations consumed ( |
posterior |
PosteriorCovariance | None
|
Optional :class: |
converged |
bool
|
optimistix's strict success flag ( |
prior_mean |
Array | None
|
Source prior mean |
prior_covariance |
Array | None
|
Source prior covariance |
control |
SpatialControl | None
|
The :class: |
background |
BackgroundAnalysis | None
|
MAP of the initial concentration and the boundary scaling,
when the problem has a :class: |
For an emission time series, the result also carries the Tier V per-event
payload (population.PerEventPosterior) for the event emission rate, the
time mean Q = wᵀS with w = 1/n_t: its Laplace posterior N(wᵀS*, wᵀP_S w) and
its prior N(wᵀS_b, wᵀBw) are exact images of the Gaussian source laws. That
time mean means nothing over cells, so a per-cell result raises
PayloadError there.
source_field
property
¶
MAP flux coefficient fields [kg m⁻² s⁻¹], shape (K, ny, nx).
Raises:
| Type | Description |
|---|---|
PayloadError
|
For an emission time series. |
emission_std
property
¶
Laplace posterior sd of Q, √(wᵀP_S w); NaN without the posterior.
samples
¶
n draws of Q from the Laplace posterior N(wᵀS*, wᵀP_S w).
The same law as averaging :func:laplace_sample source draws over
time, without re-solving for the covariance.
Raises:
| Type | Description |
|---|---|
PayloadError
|
Without the posterior ( |
Source code in src/plumax/les_fvm/fourdvar.py
prior_logpdf
¶
log π(Q) of the time-mean source prior, N(wᵀS_b, wᵀBw).
Raises:
| Type | Description |
|---|---|
PayloadError
|
If the result was built without its prior, or for a per-cell flux control. |
Source code in src/plumax/les_fvm/fourdvar.py
PosteriorCovariance
dataclass
¶
PosteriorCovariance(whitened_covariance: Array, source_covariance: Array, control: SpatialControl | None = None, control_covariance: Array | None = None)
Laplace (Gauss-Newton) posterior covariance of a 4D-Var solution.
A Gaussian approximation N(S*, P_S) to the posterior, built from the
Gauss-Newton Hessian of the whitened-space cost at the MAP χ*
(design §posterior-covariance). The whitened-space Hessian is
H_GN = I + J̃ᵀ R⁻¹ J̃, J̃ = ∂(predicted_obs)/∂χ,
so the whitened posterior covariance is P_χ = H_GN⁻¹ and — because
S = S_b + L χ — the source-space covariance is P_S = L P_χ Lᵀ. With
a :class:BackgroundControl, P_S is the source block of the covariance
of the whole flat control.
Attributes:
| Name | Type | Description |
|---|---|---|
whitened_covariance |
Array
|
|
source_covariance |
Array
|
|
control |
SpatialControl | None
|
The :class: |
control_covariance |
Array | None
|
Covariance of the whole flat control, shape
|
source_std
property
¶
Marginal source-space posterior standard deviations √diag(P_S).
bc_scale_std
property
¶
Posterior sd of the per-face factors, ordered (west, east, south, north).
Raises:
| Type | Description |
|---|---|
PayloadError
|
Without a :class: |
std_field
property
¶
Marginal posterior sd as fields [kg m⁻² s⁻¹], shape (K, ny, nx).
Raises:
| Type | Description |
|---|---|
PayloadError
|
For an emission time series. |
SpatialControl
dataclass
¶
SpatialControl(mask: Array, prior_mean: Array, length_scale_m: float, prior_std: Array, time_basis: Array | None = None, separable: bool = True)
Per-cell surface flux control with a Matérn-3/2 prior.
q(s, t) = Σ_k φ_k(t) q_k(s) on the active cells and 0 elsewhere, with independent coefficient fields sharing the spatial covariance,
q_k(s) ~ N(a_k m(s), B_s), B_s = Σ_σ ρ Σ_σ, Φ a = 1.
The coefficients a make the prior mean of the flux m(s) at every save time, so the time basis must span the constant. The flux at the save times then has covariance Φ Φᵀ ⊗ B_s, so a dense temporal prior B_t = L_t L_tᵀ is the basis Φ = L_t.
The correlation ρ depends on separable, and the two are different
kernels. With d = s − s′ and h(r) = (1 + √3 r / ℓ) exp(−√3 r / ℓ):
separable=Trueis the product kernel ρ = h(|d_y|) h(|d_x|), stored asgaussx.Kronecker(ρ_y, ρ_x)when every cell is active.separable=Falseis the isotropic kernel ρ = h(‖d‖), the dense :func:~plumax.lagrangian.inversion.matern32_covariance.
The prior is Gaussian, so the MAP flux is not constrained to be positive.
Attributes:
| Name | Type | Description |
|---|---|---|
mask |
Array
|
Active T-cells, shape |
prior_mean |
Array
|
Prior mean flux m [kg m⁻² s⁻¹], shape |
length_scale_m |
float
|
Correlation length ℓ [m]. |
prior_std |
Array
|
Prior standard deviation σ [kg m⁻² s⁻¹], shape |
time_basis |
Array | None
|
Basis Φ at the save times, shape |
separable |
bool
|
Use the product kernel rather than the isotropic one. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the shapes disagree, no cell is active, |
Examples:
Two active cells of a 2 × 2 grid, constant in time:
>>> import jax.numpy as jnp
>>> control = SpatialControl(
... mask=jnp.array([[True, False], [False, True]]),
... prior_mean=jnp.ones((2, 2)),
... length_scale_m=100.0,
... prior_std=0.5 * jnp.ones((2, 2)),
... )
>>> control.size
2
>>> control.to_field(jnp.array([2.0, 3.0])).tolist()
[[[2.0, 0.0], [0.0, 3.0]]]
flat_prior_mean
¶
Prior mean μ_q of the flat control, shape (size,).
Source code in src/plumax/les_fvm/fourdvar.py
to_field
¶
Map a flat control vector onto the grid, with 0 on inactive cells.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flat
|
Array
|
Flat control values, shape |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The coefficient fields, shape |
Source code in src/plumax/les_fvm/fourdvar.py
flux_function
¶
The flux t -> q(·, t) of a flat control, on the (ny, nx) grid.
Source code in src/plumax/les_fvm/fourdvar.py
prior_covariance
¶
Prior covariance of the flat control, I_K ⊗ Σ_σ ρ Σ_σ.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Array
|
T-cell centres along y [m], shape |
required |
x
|
Array
|
T-cell centres along x [m], shape |
required |
Returns:
| Type | Description |
|---|---|
AbstractLinearOperator
|
The covariance, with ρ a |
AbstractLinearOperator
|
kernel is separable and every cell is active, dense otherwise. |
Source code in src/plumax/les_fvm/fourdvar.py
prior_sqrt
¶
Lower-triangular square root S of :meth:prior_covariance, S Sᵀ = B.
Factorises each block with gaussx.safe_cholesky, whose adaptive
jitter survives a smooth Matérn on a fine grid; a Kronecker block needs
only its two 1-D factors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
Array
|
T-cell centres along y [m], shape |
required |
x
|
Array
|
T-cell centres along x [m], shape |
required |
Returns:
| Type | Description |
|---|---|
AbstractLinearOperator
|
S = (I_K ⊗ Σ_σ) (I_K ⊗ chol ρ). |
Source code in src/plumax/les_fvm/fourdvar.py
TemporalControl
dataclass
¶
Prior of the scalar emission time series at a known source.
The control of :func:build_problem when the forward has a source location.
Attributes:
| Name | Type | Description |
|---|---|---|
prior_mean |
Array
|
Source prior mean |
prior_covariance |
Array
|
Source prior covariance |
PlumeGrid3D
¶
Bases: Module
Arakawa C-grid + cached interior coordinate arrays.
Attributes¶
grid : CartesianGrid3D Underlying finitevolX grid. All field operators (advection, diffusion, interpolation, difference) take this as input. x : Float[Array, "nx_interior"] Interior T-point x-coordinates [m]. y : Float[Array, "ny_interior"] Interior T-point y-coordinates [m]. z : Float[Array, "nz_interior"] Interior T-point z-coordinates [m].
Notes¶
x, y, z are the interior cell-centre coordinates — they
drop the one-cell ghost rings on either side of each axis. The sizes
therefore match the interior slice shapes used by the advection and
diffusion operators, not the raw field-array shapes.
GaussianSource
¶
Bases: Module
Gaussian point source with constant or time-varying emission rate.
Attributes¶
emission_fn : Callable[[Float[Array, '']], Float[Array, '']]
Pure function t -> q(t) returning the instantaneous emission
rate [kg/s]. For a constant rate, pass a closure that discards
t.
density : Float[Array, 'nz ny nx']
Interior-shaped unit-mass spatial density [1/m³] centred at the
release point. Integrates to 1/Δx/Δy/Δz in discrete grid
units (so that q · density * Δx Δy Δz sums to q).
SurfaceFluxSource
¶
Bases: Module
Per-cell surface flux injected into the lowest interior layer.
∂C/∂t ⊇ q(s, t) / Δz in the lowest layer and 0 above it, so the tendency integrated over the cell volume Δx Δy Δz is the flux through the cell's base area, q Δx Δy [kg/s], whatever the layer thickness.
Attributes:
| Name | Type | Description |
|---|---|---|
flux_fn |
Callable[[Float[Array, '']], Float[Array, 'ny nx']]
|
Pure function |
layer_thickness |
float
|
Thickness Δz of the lowest layer [m]. |
n_layers |
int
|
Number of interior layers. |
Examples:
The tendency sits in the lowest layer, scaled by the thickness:
>>> import jax.numpy as jnp
>>> source = SurfaceFluxSource(lambda t: 2.0 * jnp.ones((3, 4)), 4.0, 2)
>>> tendency = source(jnp.asarray(0.0))
>>> tendency.shape
(4, 5, 6)
>>> float(tendency[1, 1, 1]), float(tendency[2, 1, 1])
(0.5, 0.0)
PrescribedWindField
¶
Bases: Module
Wind velocity evaluator on the full [Nz, Ny, Nx] field layout.
The interior field values are populated by _interior_fn; ghost
rings are padded with the interior boundary values so downstream
advection operators see a well-defined field everywhere.
Parameters¶
plume_grid : PlumeGrid3D
Grid the field lives on.
interior_fn : Callable[[float], tuple[Float[Array, 'nz ny nx'], ...]]
Pure function t -> (u_int, v_int, w_int) returning interior
velocity components at the T/U/V stagger points (the les_fvm
convention). See :func:uniform_wind_field for the simplest
example.
advection_tendency
¶
advection_tendency(concentration: Float[Array, 'Nz Ny Nx'], u: Float[Array, 'Nz Ny Nx'], v: Float[Array, 'Nz Ny Nx'], w: Float[Array, 'Nz Ny Nx'], plume_grid: PlumeGrid3D, method: str = 'weno5', periodic: tuple[bool, bool] = (False, False)) -> Float[Array, 'Nz Ny Nx']
Flux-form advective tendency -∇·(u C) at interior T-points.
Parameters¶
concentration : Float[Array, "Nz Ny Nx"]
Tracer at T-points.
u : Float[Array, "Nz Ny Nx"]
x-velocity at U-points (east faces).
v : Float[Array, "Nz Ny Nx"]
y-velocity at V-points (north faces).
w : Float[Array, "Nz Ny Nx"]
Vertical velocity collocated at T-points (see grid.py).
plume_grid : PlumeGrid3D
Grid the fields live on.
method : str, default "weno5"
Horizontal reconstruction scheme. Passed through to
:class:finitevolx.Advection3D.
periodic : tuple[bool, bool], default (False, False)
(x_periodic, y_periodic). On a periodic axis the normal-velocity
ghost face is wrapped (rather than left edge-padded) so the two
representations of the periodic seam share one velocity and the
open-wall fluxes cancel — otherwise a spatially-varying normal wind
breaks exact mass conservation at the seam. See
:func:~plumax.les_fvm.boundary.periodic_axes.
Returns¶
Float[Array, "Nz Ny Nx"] Advective tendency at T-points, zero on every ghost face.
Notes¶
The horizontal operator runs in finitevolX wall="open" mode, so the
lateral domain-wall face fluxes are assembled from the ghost ring
(first-order upwind against the wall, high-order method in the
interior). The caller must fill the ghost cells with the desired lateral
BC (Dirichlet / outflow / periodic) before calling — see
:func:~plumax.les_fvm.boundary.apply_boundary_conditions, which
:class:~plumax.les_fvm.dynamics.EulerianDispersionRHS runs every step.
This is what makes horizontal transport honor bc_x / bc_y.
Source code in src/plumax/les_fvm/advection.py
apply_boundary_conditions
¶
apply_boundary_conditions(field: Float[Array, 'Nz Ny Nx'], horizontal_bc: HorizontalBC, vertical_bc: VerticalBC, plume_grid: PlumeGrid3D) -> Float[Array, 'Nz Ny Nx']
Apply horizontal then vertical BCs to a 3-D tracer field.
Source code in src/plumax/les_fvm/boundary.py
build_default_concentration_bc
¶
build_default_concentration_bc(bc_x: str | tuple[str, str] | tuple[tuple[str, float], tuple[str, float]] = ('dirichlet', 'outflow'), bc_y: str | tuple[str, str] | tuple[tuple[str, float], tuple[str, float]] = 'periodic', bc_z: str | tuple[str, str] | tuple[tuple[str, float], tuple[str, float]] = ('neumann', 'neumann')) -> tuple[HorizontalBC, VerticalBC]
Build (HorizontalBC, VerticalBC) from user-facing BC specs.
Each of bc_x, bc_y, bc_z can be:
"periodic"— periodic on both faces of that axis.(west, east)(forbc_x) /(south, north)(bc_y) /(bottom, top)(bc_z) where each entry is either a BC-kind string ("dirichlet","neumann","outflow") or a(kind, value)tuple giving the Dirichlet / Neumann target.
Returns¶
tuple[HorizontalBC, VerticalBC]
Source code in src/plumax/les_fvm/boundary.py
diffusion_tendency
¶
diffusion_tendency(concentration: Float[Array, 'Nz Ny Nx'], eddy_diffusivity: EddyDiffusivity, plume_grid: PlumeGrid3D, periodic: tuple[bool, bool] = (False, False)) -> Float[Array, 'Nz Ny Nx']
Full 3-D eddy diffusion tendency ∇·(K ∇C) at interior T-points.
Parameters¶
concentration : Float[Array, "Nz Ny Nx"]
Tracer at T-points.
eddy_diffusivity : EddyDiffusivity
(K_h, K_z) diagonal diffusivity.
plume_grid : PlumeGrid3D
periodic : tuple[bool, bool], default (False, False)
(x_periodic, y_periodic). For a field K_h on a periodic
axis the coefficient ghost is wrapped (rather than left edge-padded
by :meth:EddyDiffusivity.as_arrays) so the paired periodic seam
faces share one diffusivity and their diffusive fluxes cancel. A
scalar K_h is uniform and already conserves, so it is untouched.
Returns¶
Float[Array, "Nz Ny Nx"] Diffusive tendency at T-points, zero on every ghost face.
Notes¶
The horizontal operator runs in finitevolX wall="open" mode, so the
lateral wall diffusive fluxes K_h·(C_edge − C_ghost)/dx are assembled
from the ghost ring. The caller must fill the ghost cells with the
desired lateral BC (Dirichlet / periodic) before calling — see
:func:~plumax.les_fvm.boundary.apply_boundary_conditions. This is what
makes horizontal diffusion honor bc_x / bc_y.
Source code in src/plumax/les_fvm/diffusion.py
mo_eddy_diffusivity
¶
mo_eddy_diffusivity(grid: PlumeGrid3D, surface_layer: SurfaceLayer, pbl_height: float | Float[Array, 'time y x'], *, it: int, k_h: float | Float[Array, 'nz ny nx']) -> EddyDiffusivity
Monin–Obukhov K-profile for K_z, with a caller-supplied K_h.
K_z(z) = κ u* z (1 − min(z/z_i, 1))² / φ_h(z/L).
The cubic-in-depth profile vanishes at the ground and at the boundary-layer
top, and φ_h (:func:plumax.met.surface_layer.phi_h) carries the stability:
below 1 in unstable air, above 1 in stable air. The min matters: without
it (1 − z/z_i)² grows again above z_i, and the free troposphere would mix.
In neutral air the mid-PBL value agrees with :func:pg_eddy_diffusivity class D
to within 30 % only for z_i between 110 m and 300 m, taking u from a 10 m wind
over z₀ = 0.1 m and PG's reference distance where σ_z = z_i / 2; the ratio does
not depend on the wind speed. Outside that range they disagree by construction:
K_z(z_i / 2) = κ u z_i / 8 grows linearly with z_i while the PG constant
saturates, so the ratio exceeds 4 at z_i = 1000 m. This is a comparison with an
empirical curve, not a validity limit of the profile.
Algorithm
- Take the height above ground of each T-level, z = (k + ½) Δz, the
convention :class:
~plumax.met.surface_layer.MoninObukhovuses. - Take u*, 1/L and z_i at time index
it. - Evaluate the profile on every T-point of the interior.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
PlumeGrid3D
|
The analysis grid. |
required |
surface_layer
|
SurfaceLayer
|
u* and 1/L on |
required |
pbl_height
|
float | Float[Array, 'time y x']
|
Boundary-layer height z_i [m], a scalar or
|
required |
it
|
int
|
Time index into |
required |
k_h
|
float | Float[Array, 'nz ny nx']
|
Horizontal diffusivity [m²/s], passed through: a constant, or
:func: |
required |
Returns:
| Type | Description |
|---|---|
EddyDiffusivity
|
|
Raises:
| Type | Description |
|---|---|
EquinoxRuntimeError
|
Under |
Examples:
Neutral air at mid-PBL gives κ u* z_i / 8:
>>> import jax.numpy as jnp
>>> from plumax.les_fvm.grid import make_grid
>>> from plumax.met.surface_layer import SurfaceLayer
>>> grid = make_grid((0.0, 40.0, 4), (0.0, 40.0, 4), (0.0, 160.0, 4))
>>> layer = SurfaceLayer(
... u_star=jnp.full((1, 4, 4), 0.5),
... inverse_obukhov_length=jnp.zeros((1, 4, 4)),
... w_star=jnp.zeros((1, 4, 4)),
... )
>>> eddy = mo_eddy_diffusivity(grid, layer, 120.0, it=0, k_h=1.0)
>>> eddy.vertical.shape
(4, 4, 4)
>>> bool(jnp.allclose(eddy.vertical[1], 0.4 * 0.5 * 120.0 / 8.0))
True
Above the boundary-layer top nothing mixes:
Source code in src/plumax/les_fvm/diffusion.py
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 | |
pg_eddy_diffusivity
¶
pg_eddy_diffusivity(stability_class: str, wind_speed: float, reference_distance: float = 200.0) -> EddyDiffusivity
Rough K-theory calibration from Pasquill-Gifford σ curves.
For a neutral puff travelling at u under stability class C,
the cross-wind and vertical spreads at a reference distance
x_ref are σ_y(x_ref) and σ_z(x_ref). Taylor's identity
K = σ² / (2 t) with t = x_ref / u gives a simple constant
effective diffusivity. The result is the average K over
[0, x_ref]; users wanting a sharper match across a wide range
of travel distances should pass a spatially-varying field directly.
Parameters¶
stability_class : str
One of "A".."F".
wind_speed : float
Representative wind speed u [m/s]. Must be > 0.
reference_distance : float, default 200.0
Reference downwind distance x_ref [m] at which the σ curves
are sampled.
Returns¶
EddyDiffusivity
Scalar (K_h, K_z) calibrated at x_ref.
Raises¶
ValueError
If wind_speed or reference_distance is non-positive.
Source code in src/plumax/les_fvm/diffusion.py
smagorinsky_k_h
¶
smagorinsky_k_h(wind: PrescribedWindField, grid: PlumeGrid3D, *, c_s: float = 0.2, t: float | Float[Array, '']) -> Float[Array, 'nz ny nx']
Smagorinsky horizontal diffusivity from the resolved deformation.
K_h = (c_s Δ)² |D|, Δ = √(Δx Δy), |D|² = D_T² + D_S²,
with the tension D_T = ∂u/∂x − ∂v/∂y and the shear D_S = ∂v/∂x + ∂u/∂y. These are the two independent components of the trace-free horizontal strain, so uniform translation, solid-body rotation and isotropic horizontal divergence all give K_h = 0. Vertical shear is left out on purpose: K_h only mixes along levels, and ∂u/∂z in the boundary layer would swamp it on a grid with Δz ≪ Δx.
Algorithm
- Evaluate the wind at
ton its C-grid stagger: u on U-points, v on V-points, level by level. - D_T lands on T-points; D_S lands on corners, and D_S² is averaged from the four corners to the T-point, so a checkerboard in D_S cannot cancel.
- K_h = (c_s Δ)² |D| on interior T-points.
The wind's ghost ring is an edge pad, so its normal gradient across the domain edge reads zero and the outermost interior cells are biased low; cells one further in are exact for a linear wind.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
wind
|
PrescribedWindField
|
The wind field. |
required |
grid
|
PlumeGrid3D
|
The grid it lives on. |
required |
c_s
|
float
|
Smagorinsky constant c_s (> 0). |
0.2
|
t
|
float | Float[Array, '']
|
Time [s]. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'nz ny nx']
|
K_h [m²/s] on interior T-points, the layout |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
A uniform wind has no deformation, and a shear flow has K_h = (c_s Δ)² S:
>>> import jax.numpy as jnp
>>> from plumax.les_fvm.grid import make_grid
>>> from plumax.les_fvm.wind import uniform_wind_field, wind_field_from_callable
>>> grid = make_grid((0.0, 80.0, 8), (0.0, 80.0, 8), (0.0, 40.0, 4))
>>> uniform = uniform_wind_field(grid, u=5.0, v=1.0)
>>> bool((smagorinsky_k_h(uniform, grid, t=0.0) == 0.0).all())
True
>>> shear = wind_field_from_callable(
... grid, lambda t, x, y, z: (0.1 * y, jnp.zeros_like(y), jnp.zeros_like(y))
... )
>>> k_h = smagorinsky_k_h(shear, grid, t=0.0)
>>> bool(jnp.allclose(k_h[:, 1:-1, 1:-1], (0.2 * 10.0) ** 2 * 0.1, rtol=1e-4))
True
References
Smagorinsky (1963), Mon. Wea. Rev. 91, 99–164.
Source code in src/plumax/les_fvm/diffusion.py
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 | |
build_forward
¶
build_forward(*, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], domain_z: tuple[float, float, int], save_times: Array, source_location: tuple[float, float, float] | None = None, uniform_wind: tuple[float, float, float], control: SpatialControl | None = None, eddy_diffusivity: float | tuple[float, float] = 1.0, source_radius: float | None = None, advection_scheme: str = 'upwind1', receptor_index: Array | None = None, solver_kwargs: dict | None = None, bc_x: str | tuple = ('dirichlet', 'outflow'), bc_y: str | tuple = 'periodic') -> EulerianForward4DVar
Assemble a differentiable :class:EulerianForward4DVar.
The transport configuration mirrors
:func:~plumax.les_fvm.simulate_eulerian_dispersion but is frozen except
for the emission time series, which becomes the 4D-Var control vector.
A uniform wind and scalar / anisotropic constant diffusivity keep the v1
inversion fast and the twin experiment exactly reproducible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
domain_x
|
tuple[float, float, int]
|
|
required |
domain_y
|
tuple[float, float, int]
|
|
required |
domain_z
|
tuple[float, float, int]
|
|
required |
save_times
|
Array
|
Observation/integration times [s], shape |
required |
source_location
|
tuple[float, float, float] | None
|
|
None
|
uniform_wind
|
tuple[float, float, float]
|
Constant |
required |
control
|
SpatialControl | None
|
Per-cell surface flux control, injected through
:class: |
None
|
eddy_diffusivity
|
float | tuple[float, float]
|
Constant scalar |
1.0
|
source_radius
|
float | None
|
Gaussian source radius [m] (defaults to |
None
|
advection_scheme
|
str
|
Reconstruction scheme ( |
'upwind1'
|
receptor_index
|
Array | None
|
Optional |
None
|
solver_kwargs
|
dict | None
|
Overrides for the diffrax solve. |
None
|
bc_x
|
str | tuple
|
West and east boundary conditions, as
:func: |
('dirichlet', 'outflow')
|
bc_y
|
str | tuple
|
South and north boundary conditions, likewise. |
'periodic'
|
Returns:
| Type | Description |
|---|---|
EulerianForward4DVar
|
A configured :class: |
Source code in src/plumax/les_fvm/fourdvar.py
600 601 602 603 604 605 606 607 608 609 610 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 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 | |
build_problem
¶
build_problem(*, forward: EulerianForward4DVar, observations: Array, prior_mean: Array | None = None, prior_covariance: Array | None = None, obs_variance: Array | float, control: SpatialControl | TemporalControl | None = None, background: BackgroundControl | None = None) -> FourDVarProblem
Assemble a :class:FourDVarProblem, factorising the prior covariance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forward
|
EulerianForward4DVar
|
The differentiable transport forward. |
required |
observations
|
Array
|
Column observations |
required |
prior_mean
|
Array | None
|
Source prior mean |
None
|
prior_covariance
|
Array | None
|
Source prior covariance |
None
|
obs_variance
|
Array | float
|
Observation-error variance |
required |
control
|
SpatialControl | TemporalControl | None
|
The control and its prior: the same :class: |
None
|
background
|
BackgroundControl | None
|
Adds the initial concentration and the per-face boundary
scaling to the control, with a block-diagonal prior. |
None
|
Returns:
| Type | Description |
|---|---|
FourDVarProblem
|
The configured :class: |
Source code in src/plumax/les_fvm/fourdvar.py
laplace_sample
¶
laplace_sample(problem: FourDVarProblem, whitened_map: Array, key: Array, n_samples: int) -> jax.Array
Draw source-space samples from the Laplace posterior N(S*, P_S).
The draws come from vardax.Posterior.sample on the Gauss–Newton
Laplace posterior at the MAP source, S = S* + P_S^½ ξ with
ξ ~ N(0, I); P_S is the covariance of :func:posterior_covariance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
FourDVarProblem
|
The assembled 4D-Var problem. |
required |
whitened_map
|
Array
|
The MAP whitened control |
required |
key
|
Array
|
A |
required |
n_samples
|
int
|
Number of posterior draws to return. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
Source-space samples, shape |
Array
|
|
Source code in src/plumax/les_fvm/fourdvar.py
posterior_covariance
¶
Gauss-Newton Laplace posterior covariance around a 4D-Var MAP.
vardax's LaplaceCovariance of the whitened control at
whitened_map: the Gauss–Newton precision I + J̃ᵀ R⁻¹ J̃ in χ,
mapped to the source as P_S = L P_χ Lᵀ for the prior Cholesky factor
L.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
FourDVarProblem
|
The assembled 4D-Var problem. |
required |
whitened_map
|
Array
|
The MAP whitened control |
required |
Returns:
| Name | Type | Description |
|---|---|---|
The |
PosteriorCovariance
|
class: |
Raises:
| Type | Description |
|---|---|
ImportError
|
Without the |
Source code in src/plumax/les_fvm/fourdvar.py
solve_4dvar
¶
solve_4dvar(problem: FourDVarProblem, *, method: Literal['lbfgs', 'gauss_newton'] = 'lbfgs', initial_source: Array | None = None, rtol: float = 1e-06, atol: float = 1e-08, max_steps: int = 100, solver: AbstractMinimiser | None = None, compute_posterior: bool = False, outer_iters: int = 5, inner_iters: int = 50) -> FourDVarResult
Minimise the 4D-Var cost in whitened space, via vardax.
method="lbfgs" runs :meth:FourDVarProblem.analysis:
vardax.ThreeDVar over the whitened control χ, so the prior is the
identity Gaussian and the gradient is the exact discrete adjoint of the FV
solver.
method="gauss_newton" runs :meth:FourDVarProblem.incremental_analysis:
vardax.IncrementalFourDVar, Gauss–Newton outer iterations with CG inner
iterations over the same whitened cost. The forward is affine in the
control, so the linearised cost is the cost itself and one outer iteration
with a converged CG solve reaches the MAP. Each outer iteration forms the
whole Jacobian, one VJP per observation, so it costs more than L-BFGS when
the observations are many and the problem is well conditioned.
The observation-space dual (PSAS) is not offered: it belongs to vardax (jejjohnson/vardax#72).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
FourDVarProblem
|
The assembled 4D-Var problem. |
required |
method
|
Literal['lbfgs', 'gauss_newton']
|
The minimiser. |
'lbfgs'
|
initial_source
|
Array | None
|
Optional first-guess emission signal |
None
|
rtol
|
float
|
Relative tolerance of the optimiser, or of the CG solves. |
1e-06
|
atol
|
float
|
Absolute tolerance of the optimiser, or of the CG solves. |
1e-08
|
max_steps
|
int
|
Maximum L-BFGS iterations. |
100
|
solver
|
AbstractMinimiser | None
|
Optional optimistix minimiser (defaults to |
None
|
compute_posterior
|
bool
|
When |
False
|
outer_iters
|
int
|
Gauss–Newton outer iterations. |
5
|
inner_iters
|
int
|
CG iterations per Gauss–Newton outer iteration. |
50
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
FourDVarResult
|
class: |
FourDVarResult
|
|
|
FourDVarResult
|
|
|
FourDVarResult
|
whether ‖∇𝒥(χ*)‖ ≤ atol + rtol ‖∇𝒥(0)‖, because vardax reports none. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown |
ImportError
|
Without the |
Source code in src/plumax/les_fvm/fourdvar.py
1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 | |
make_grid
¶
make_grid(domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], domain_z: tuple[float, float, int], dtype: dtype = jnp.float32) -> PlumeGrid3D
Build a :class:PlumeGrid3D from (x_min, x_max, nx_interior) triples.
Parameters¶
domain_x : tuple (x_min, x_max, nx_interior)
Physical x-range + number of interior cells in x.
domain_y : tuple (y_min, y_max, ny_interior)
Physical y-range + number of interior cells in y.
domain_z : tuple (z_min, z_max, nz_interior)
Physical z-range + number of interior cells in z. z_min is
treated as the ground level (Neumann-wall BC target below).
dtype : jnp.dtype, default float32
Coordinate-array dtype.
Returns¶
PlumeGrid3D
Raises¶
ValueError If any interior cell count is < 4 (WENO5 stencils need at least four interior cells in every horizontal direction) or if any physical extent is non-positive.
Source code in src/plumax/les_fvm/grid.py
simulate_eulerian_dispersion
¶
simulate_eulerian_dispersion(*, domain_x: tuple[float, float, int], domain_y: tuple[float, float, int], domain_z: tuple[float, float, int], t_start: float, t_end: float, save_interval: float, emission_rate: float | Callable[[Float[Array, '']], Float[Array, '']], source_location: tuple[float, float, float], source_radius: float | None = None, wind_schedule: WindSchedule | None = None, uniform_wind: tuple[float, float, float] | None = None, wind_field: PrescribedWindField | None = None, eddy_diffusivity: float | tuple[float, float] | str | EddyDiffusivity = 'pg', stability_class: str | None = None, pg_reference_distance: float = 200.0, advection_scheme: AdvectionScheme = 'weno5', bc_x: str | tuple[str, str] | tuple[tuple[str, float], tuple[str, float]] = ('dirichlet', 'outflow'), bc_y: str | tuple[str, str] | tuple[tuple[str, float], tuple[str, float]] = 'periodic', bc_z: str | tuple[str, str] | tuple[tuple[str, float], tuple[str, float]] = ('neumann', 'neumann'), solver: SolverName = 'tsit5', dt0: float = 1.0, rtol: float = 0.001, atol: float = 1e-08, max_steps: int = 100000, max_wind_speed: float | None = None, initial_concentration: Float[Array, 'Nz Ny Nx'] | None = None, seed: int = 0) -> xr.Dataset
Simulate 3-D Eulerian tracer transport under a prescribed wind.
Parameters¶
domain_x, domain_y, domain_z : tuple (min, max, n_interior)
Physical extent + number of interior cells per axis.
t_start, t_end : float
Simulation window [s].
save_interval : float
Save cadence [s]. Saved times are
np.arange(t_start, t_end + save_interval, save_interval).
emission_rate : float or callable
Methane source rate [kg/s] (constant or t -> q(t)).
source_location : tuple (x, y, z)
Release point [m].
source_radius : float, optional
Gaussian source radius [m]. Defaults to 2 · max(dx, dy, dz).
wind_schedule : WindSchedule, optional
uniform_wind : (u, v, w), optional
wind_field : PrescribedWindField, optional
One of these three must be provided — they are mutually exclusive.
eddy_diffusivity : float | (K_h, K_z) | "pg" | EddyDiffusivity
K-theory eddy diffusivity spec. The string "pg" pulls a rough
calibration from the PG σ curves — requires stability_class and
a non-zero mean wind speed.
stability_class : str, optional
Required when eddy_diffusivity="pg".
pg_reference_distance : float, default 200.0
Reference downwind distance for PG → K-theory conversion.
advection_scheme : {"upwind1", "weno3", "weno5", ...}, default "weno5"
Horizontal reconstruction scheme.
bc_x, bc_y, bc_z : BC specs
See :func:boundary.build_default_concentration_bc.
solver : {"tsit5", "dopri5", "ssprk3", "heun"}, default "tsit5"
diffrax time integrator. "ssprk3" and "heun" are
explicit-fixed-step and ignore rtol/atol.
dt0 : float
Initial / fixed step size [s].
rtol, atol : float
Adaptive-step controller tolerances (ignored for fixed-step solvers).
max_steps : int
Upper bound on diffrax internal steps.
max_wind_speed : float, optional
Conservative upper bound on each wind component |u|, |v|, |w|
[m/s], used only by the fixed-step stability guard. Supply it for a
callable wind whose peak the guard cannot otherwise sample reliably;
when None the peak is estimated from the field (dense time grid +
any WindSchedule knots in the window).
initial_concentration : ndarray, optional
Interior-shaped (nz, ny, nx) initial tracer field. Defaults
to zero.
seed : int
Currently unused (reserved for future stochastic variants).
Returns¶
xr.Dataset
concentration(time, z, y, x) [kg/m³] and the diagnostic
column_concentration(time, y, x) [kg/m²]. The dataset
carries model metadata in ds.attrs.
Raises¶
ValueError For inconsistent wind/source/emission specs.
Source code in src/plumax/les_fvm/simulate.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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 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 | |
stable_step_bound
¶
stable_step_bound(*, dx: float, dy: float, dz: float, max_u: float, max_v: float, max_w: float, k_h: float, k_z: float, cfl_safety: float = 1.0) -> float
Maximum stable step for an explicit fixed-step transport solve.
Advection and diffusion are advanced in the same explicit step, so their tendencies add and the conservative forward-Euler / Heun limit is set by the summed rate::
adv_rate = |u|/dx + |v|/dy + |w|/dz
diff_rate = 2·(K_h/dx² + K_h/dy² + K_z/dz²)
bound = cfl_safety / (adv_rate + diff_rate)
Using min(1/adv_rate, 1/diff_rate) instead would permit up to a 2×
step when the two rates are comparable. The bound is inf only when
the field is both motionless and diffusion-free.
Parameters¶
dx, dy, dz : float
Grid spacing [m].
max_u, max_v, max_w : float
Component-wise maximum wind speed |u|, |v|, |w| over the run [m/s].
k_h, k_z : float
Maximum horizontal / vertical eddy diffusivity [m²/s].
cfl_safety : float, default 1.0
Multiplier on the raw stability bound. 1.0 is the forward-Euler
sum-CFL / FTCS-diffusion limit; the RK schemes used here are at
least as stable, so this is a conservative guard.
Returns¶
float
The maximum stable dt0 [s].
Notes¶
This is a first-order (upwind / forward-Euler) sum-CFL estimate — a
conservative safety net that catches gross step-size errors (the
dt0-orders-of-magnitude-too-large failure mode). It is not a
rigorous stability certificate for the higher-order or low-dissipation
reconstructions (e.g. the default weno5, or naive): those
semidiscrete operators have different stability regions, and an explicit
RK step can amplify their weakly-damped modes even below this bound. Keep
dt0 comfortably under the reported value when using such schemes.
Source code in src/plumax/les_fvm/simulate.py
make_gaussian_source
¶
make_gaussian_source(plume_grid: PlumeGrid3D, emission_rate: float | Callable[[Float[Array, '']], Float[Array, '']], source_location: tuple[float, float, float], source_radius: float | None = None) -> GaussianSource
Build a :class:GaussianSource on plume_grid.
Parameters¶
plume_grid : PlumeGrid3D
emission_rate : float or callable
Either a constant rate [kg/s], or a pure function t -> q(t)
that returns an instantaneous rate as a scalar JAX array.
source_location : tuple (x, y, z)
Release point [m]. Must lie inside the interior domain.
source_radius : float, optional
Isotropic Gaussian radius [m]. Defaults to 2 · max(dx, dy, dz)
— wide enough to avoid single-cell aliasing without smearing the
plume.
Returns¶
GaussianSource
Raises¶
ValueError
If emission_rate is a constant that is negative, or if the
source location lies outside the interior domain.
Source code in src/plumax/les_fvm/source.py
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 | |
uniform_wind_field
¶
uniform_wind_field(plume_grid: PlumeGrid3D, u: float = 0.0, v: float = 0.0, w: float = 0.0) -> PrescribedWindField
Space- and time-constant wind field.
Parameters¶
plume_grid : PlumeGrid3D u, v, w : float, default 0.0 Cartesian wind velocity components [m/s].
Returns¶
PrescribedWindField
Raises¶
ValueError
If any of u, v, w is non-finite — a NaN/inf wind would make
the CFL bound NaN (disabling the fixed-step guard) and corrupt the solve.
Source code in src/plumax/les_fvm/wind.py
wind_field_from_callable
¶
wind_field_from_callable(plume_grid: PlumeGrid3D, fn: Callable[[Float[Array, ''], Float[Array, 'nz ny nx'], Float[Array, 'nz ny nx'], Float[Array, 'nz ny nx']], tuple[Float[Array, 'nz ny nx'], Float[Array, 'nz ny nx'], Float[Array, 'nz ny nx']]]) -> PrescribedWindField
General-purpose wind field from a user-supplied (t, X, Y, Z) function.
Each velocity component is sampled at its C-grid stagger point so the field the advection operator consumes is correctly located:
uat U-points — T-point coordinates shifted+dx/2in x,vat V-points — T-point coordinates shifted+dy/2in y,wat T-points (collocated, per theles_fvmconvention).
fn is therefore evaluated three times — once per stagger — and
only the matching component is kept from each call. A divergence-free
analytic wind sampled this way stays discretely divergence-free
(sampling all three at T-points would displace u/v by half a
cell and introduce spurious tracer sources).
Parameters¶
plume_grid : PlumeGrid3D
fn : callable
Pure function (t, X, Y, Z) -> (u, v, w) where each of
X, Y, Z is an interior-shaped coordinate array. Each output
must have the same interior shape.
Returns¶
PrescribedWindField
Source code in src/plumax/les_fvm/wind.py
wind_field_from_schedule
¶
wind_field_from_schedule(plume_grid: PlumeGrid3D, schedule: WindSchedule, w: float = 0.0) -> PrescribedWindField
Time-varying but spatially uniform wind field.
u(t) and v(t) are interpolated from the WindSchedule
(shared with gauss_puff); w remains constant in time and
space (0 by default).
Parameters¶
plume_grid : PlumeGrid3D schedule : WindSchedule w : float, default 0.0 Constant vertical velocity [m/s].
Returns¶
PrescribedWindField