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 emission term.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
Methane source term.
horizontal_bc : HorizontalBC
vertical_bc : VerticalBC
advection_scheme : str, default "weno5"
Horizontal reconstruction scheme for
:func:advection.advection_tendency.
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.
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[[Array], EulerianDispersionRHS], observation_op: ColumnObservationOperator, save_times: Array, initial_concentration: Array, t0: float, t1: float, solver_kwargs: dict = dict())
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 — 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[[Array], 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). |
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)
A strong-constraint 4D-Var problem over a scalar emission time series.
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.
source_from_whitened
¶
cost
¶
The 4D-Var cost J at whitened control χ (scalar).
Source code in src/plumax/les_fvm/fourdvar.py
value_and_grad
¶
(J, ∂J/∂χ) — the gradient is the exact adjoint via reverse-mode AD.
FourDVarResult
dataclass
¶
FourDVarResult(source: Array, whitened: Array, cost: float, n_steps: int, posterior: PosteriorCovariance | None = None, converged: bool = True)
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
|
optx's strict success flag ( |
PosteriorCovariance
dataclass
¶
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ᵀ.
Attributes:
| Name | Type | Description |
|---|---|---|
whitened_covariance |
Array
|
|
source_covariance |
Array
|
|
source_std
property
¶
Marginal source-space posterior standard deviations √diag(P_S).
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).
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
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
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], uniform_wind: tuple[float, float, float], 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) -> 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]
|
|
required |
uniform_wind
|
tuple[float, float, float]
|
Constant |
required |
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
|
Returns:
| Type | Description |
|---|---|
EulerianForward4DVar
|
A configured :class: |
Source code in src/plumax/les_fvm/fourdvar.py
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 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 | |
build_problem
¶
build_problem(*, forward: EulerianForward4DVar, observations: Array, prior_mean: Array, prior_covariance: Array, obs_variance: Array | float) -> 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
|
Source prior mean |
required |
prior_covariance
|
Array
|
Source prior covariance |
required |
obs_variance
|
Array | float
|
Observation-error variance |
required |
Returns:
| Type | Description |
|---|---|
FourDVarProblem
|
The configured :class: |
Source code in src/plumax/les_fvm/fourdvar.py
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 | |
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).
Sampling happens in whitened space — χ ~ N(χ*, P_χ) via a Cholesky
factor of P_χ — and each draw is mapped through
:meth:FourDVarProblem.source_from_whitened, which is exactly the affine
map that turns N(χ*, P_χ) into N(S*, P_S).
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 |
Source code in src/plumax/les_fvm/fourdvar.py
posterior_covariance
¶
Gauss-Newton Laplace posterior covariance around a 4D-Var MAP.
Builds the Gauss-Newton Hessian H_GN = I + J̃ᵀ R⁻¹ J̃ of the whitened
cost at whitened_map and inverts it with :mod:gaussx (the Hessian is
wrapped as a symmetric positive-semidefinite
:class:lineax.MatrixLinearOperator and inverted via :func:gaussx.inv).
The whitened posterior covariance is P_χ = H_GN⁻¹ and the source-space
covariance is 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: |
Source code in src/plumax/les_fvm/fourdvar.py
solve_4dvar
¶
solve_4dvar(problem: FourDVarProblem, *, 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) -> FourDVarResult
Minimise the 4D-Var cost with L-BFGS in whitened space.
Optimises χ (whitened control), so the prior is the identity Gaussian
and the gradient — the exact discrete adjoint of the FV solver — flows
through jax.value_and_grad of :meth:FourDVarProblem.cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
problem
|
FourDVarProblem
|
The assembled 4D-Var problem. |
required |
initial_source
|
Array | None
|
Optional first-guess emission signal |
None
|
rtol
|
float
|
Relative optimiser tolerance. |
1e-06
|
atol
|
float
|
Absolute optimiser tolerance. |
1e-08
|
max_steps
|
int
|
Maximum optimiser iterations. |
100
|
solver
|
AbstractMinimiser | None
|
Optional optimistix minimiser (defaults to |
None
|
compute_posterior
|
bool
|
When |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
FourDVarResult
|
class: |
Source code in src/plumax/les_fvm/fourdvar.py
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 | |
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
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