Operators API
For narrative coverage of the operator hierarchy, the apply! contract, and the Strang palindrome, see Operators. For per-scheme advection properties, Advection schemes.
Operators (top-level)
AtmosTransport.Operators Module
OperatorsPhysics operators for the basis-explicit transport architecture.
Provides:
Abstract operator types (
AbstractOperator,AbstractDiffusion, etc.)Advection hierarchy:
AbstractAdvectionScheme→AbstractConstantScheme,AbstractLinearScheme,AbstractQuadraticSchemewith concreteUpwindScheme,SlopesScheme, and structured-gridPPMSchemeStrang splitting orchestrator:
strang_split!,apply!
AtmosTransport.Operators.AbstractConvection Type
AbstractConvection <: AbstractOperatorRoot type for convective-transport operators. Concrete subtypes live in src/Operators/Convection/ (NoConvection, CMFMCConvection, TM5Convection).
AtmosTransport.Operators.AbstractDiffusion Type
AbstractDiffusion <: AbstractOperatorRoot type for vertical diffusion operators. Concrete subtypes live in src/Operators/Diffusion/operators.jl (NoDiffusion, ImplicitVerticalDiffusion, …).
AtmosTransport.Operators.AbstractOperator Type
AbstractOperatorRoot type for all physics operators in the transport model.
sourceAtmosTransport.Operators.AbstractStepRecorder Type
AbstractStepRecorderObserver of one model step. Production passes NoStepRecorder; the cubed-sphere adjoint passes a recorder that writes its tape from the same calls. Hooks (record_halo!, record_sweep!, record_center!, record_diffusion!, record_emission!, record_linrood_horizontal!, record_convection!, record_air_mass_reset!, record_chemistry!) read their arguments and never modify model state.
AtmosTransport.Operators.NoStepRecorder Type
NoStepRecorder()The production recorder: every hook is an inlined no-op.
sourceAdvection
AtmosTransport.Operators.Advection Module
AdvectionAdvection operators for the basis-explicit transport architecture.
Provides:
Scheme hierarchy:
UpwindScheme <: AbstractConstantScheme— first-order upwind via generic kernelsSlopesScheme <: AbstractLinearScheme— van Leer slopes (limiter-dispatched)PPMScheme <: AbstractQuadraticScheme— structured-grid PPM (not yet an official real-data reference path)LinRoodPPMScheme <: AbstractAdvectionScheme— cubed-sphere FV3/Lin-Rood PPM pathAbstractLimitersubtypes:NoLimiter,MonotoneLimiter,PositivityLimiter,CW84Limiter(PPM only)
Multi-tracer optimization:
TracerView— zero-cost 3D slice adapter for 4D tracer arraysMulti-tracer kernel shells fuse the N-tracer loop into GPU kernels, reducing launches from 6N to 6 per Strang split
Infrastructure:
AdvectionWorkspace+strang_split!— Strang splitting orchestratorCFL utilities for subcycling decisions
AtmosTransport.Operators.Advection.AbstractAdvectionScheme Type
AbstractAdvectionSchemeRoot abstract type for all advection operators in the mass-flux transport core.
Every concrete scheme belongs to one of three reconstruction families:
AbstractAdvectionScheme
├── AbstractConstantScheme (order 0: donor-cell / upwind)
├── AbstractLinearScheme (order 1: van Leer slopes / MUSCL)
└── AbstractQuadraticScheme (order 2: PPM / Prather moments)This hierarchy enables orthogonal dispatch:
Reconstruction order selects the face-flux
@inlinefunctionLimiter type (carried as a type parameter) selects the slope/moment limiter
Grid topology is handled by the kernel shell (structured vs face-indexed)
CS execution style is handled separately (
strang_split_cs!sweep shell vs Lin-Rood / FV3 horizontal update)Backend (CPU/GPU) is handled by KernelAbstractions.jl
Implementing a new scheme
Subtype one of the three families
Implement
_xface_tracer_flux,_yface_tracer_flux,_zface_tracer_flux(seereconstruction.jl)The universal kernel shells in
structured_kernels.jlwill automatically dispatch to your face-flux functions at compile time
Example
scheme = SlopesScheme(MonotoneLimiter())
strang_split!(state, fluxes, grid, scheme; workspace=ws)AtmosTransport.Operators.Advection.AbstractBinaryCFLCheck Type
AbstractBinaryCFLCheckWhether a cubed-sphere split-sweep step that takes its subcycle count from the transport binary's substep schedule recomputes the runtime CFL budget and compares. Set from [advection] check_binary_cfl; carried by CSAdvectionWorkspace.
AtmosTransport.Operators.Advection.AbstractConstantScheme Type
AbstractConstantScheme <: AbstractAdvectionSchemePiecewise-constant (order 0) reconstruction family.
The face value equals the donor cell mean — the cell upwind of the mass flux. This is the simplest conservative finite-volume scheme and the reference implementation for the generic kernel shells.
Concrete subtypes: UpwindScheme
AtmosTransport.Operators.Advection.AbstractLimiter Type
AbstractLimiterPolicy object controlling slope and moment limiting in linear and quadratic advection schemes.
Limiters are carried as type parameters on scheme structs (e.g., SlopesScheme{MonotoneLimiter}), enabling compile-time specialization with zero runtime branches on GPU.
Available limiters:
NoLimiter: unlimited centered slopes (second-order, may oscillate)MonotoneLimiter: van Leer minmod (monotone, TVD)PositivityLimiter: ensures non-negative face valuesCW84Limiter: the complete Colella–Woodward PPM (PPMSchemeonly)
See limiters.jl for the @inline implementations.
AtmosTransport.Operators.Advection.AbstractLinearScheme Type
AbstractLinearScheme <: AbstractAdvectionSchemePiecewise-linear (order 1) reconstruction family (van Leer 1977, MUSCL).
The subcell profile in each cell is _slopes_face_flux in reconstruction.jl).
Concrete subtypes: SlopesScheme
AtmosTransport.Operators.Advection.AbstractQuadraticScheme Type
AbstractQuadraticScheme <: AbstractAdvectionSchemePiecewise-quadratic (order 2) reconstruction family.
Includes PPM (Colella & Woodward 1984; Putman & Lin 2007) and Prather second-moment schemes. The subcell profile is a parabola constrained by the cell mean and (limited) edge values.
Concrete subtypes: PPMScheme
AtmosTransport.Operators.Advection.AbstractVerticalReconstruction Type
AbstractVerticalReconstructionHow PPMScheme reconstructs the tracer profile in the vertical sweep. The horizontal sweeps are unaffected.
AtmosTransport.Operators.Advection.AdvectionWorkspace Type
AdvectionWorkspace{FT, A, V1, A4}Pre-allocated buffers for mass-flux Strang splitting. Eliminates all array allocations from the inner time-stepping loop.
The workspace provides TWO complete buffer pairs ((rm_A, m_A) and (rm_B, m_B)) used as ping-pong source/destination by strang_split!. Each directional sweep reads from one pair and writes to the other; the palindrome's six sweeps flip parity an even number of times, so the caller's home arrays receive the final result naturally.
Kernels always write to a DIFFERENT array than they read from — this is the double-buffer contract, and in-place updates would violate the stencil's read-before-write assumption and break mass conservation by ~10% per step.
Fields
rm_A::A,rm_B::A— 3D tracer-mass ping-pong pair (same size asrm)m_A::A,m_B::A— 3D air-mass ping-pong pair (same size asm)cluster_sizes::V1— per-latitude clustering factors for reduced grids (Int32[Ny]; all ones for uniform grids; empty for face-indexed meshes)face_left::V1,face_right::V1— face connectivity for face-indexed meshesrm_4d_A::A4,rm_4d_B::A4— 4D tracer-mass ping-pong pair for the multi-tracer fused path ((Nx, Ny, Nz, Nt)). Both are allocated to size 0×0×0×0 whenn_tracers == 0.
Constructors
AdvectionWorkspace(m::AbstractArray{FT,3}; cluster_sizes_cpu=nothing, n_tracers=0)Create workspace for a 3D structured grid, allocating both buffer pairs matching the size of m. If cluster_sizes_cpu is nothing, defaults to uniform (all ones).
AdvectionWorkspace(m::AbstractArray{FT,2}; cluster_sizes_cpu=nothing, mesh=nothing)Create workspace for a 2D face-indexed grid (cell × level layout).
sourceAtmosTransport.Operators.Advection.AdvectionWorkspace Method
AdvectionWorkspace(state::CellState; cluster_sizes_cpu=nothing, mesh=nothing)Construct a workspace sized for state: infers n_tracers from ntracers(state) so the 4D ping-pong buffers match the packed tracer storage. This is the preferred form; the raw AdvectionWorkspace(m; n_tracers=…) is kept for low-level callers.
AtmosTransport.Operators.Advection.BinaryCFLCheck Type
BinaryCFLCheck()Recompute the palindrome CFL subcycle count at every step and throw an ArgumentError when the binary's schedule asks for fewer subcycles. One extra reduction over all panels per step; a diagnostic for binaries of unknown origin.
AtmosTransport.Operators.Advection.CSAdvectionWorkspace Type
CSAdvectionWorkspace{FT, A, P3, A4, P4, C}Pre-allocated cubed-sphere transport workspace.
rm_A,m_Aare the halo-padded single-tracer advection ping-pong buffers shared across panels.rm_4d_Ais the packed-tracer panel buffer used by the production split-sweep path so CS follows the same packedtracers_rawparadigm as structured grids.m_pp_buf,rm_4d_pp_bufare full-panel spare buffers for the packed ping-pong path, avoiding the per-sweep copy-back kernels.seam_fluxholds one air and N-tracer transfer per physical panel edge; its storage scales with edge length, not panel area. Lin–Rood workspaces disable this split-sweep cache (seam_transport=false).column_scratchis the per-column, per-tracer working storage of the FV3 vertical profile (Nc × Nc × (Nz+1) × 3 max(Nt_s, 1) G,Nt_s = column_scratch_tracers, the packed tracer count by default, andG = column_scratch_panelsthe panels one packed sweep launch covers, by default_fv3_sweep_panel_group(Nc, Nt_s)); it is empty unless the workspace is built withcolumn_scratch=true.max_subcyclestracks this workspace's high-water mark for CFL diagnostics; keeping it with the workspace prevents unrelated simulations sharing state.binary_cfl_check(NoBinaryCFLCheck()by default, orBinaryCFLCheck()) decides whether a subcycle count from the binary is checked against the runtime CFL budget.
AtmosTransport.Operators.Advection.CW84Limiter Type
CW84Limiter <: AbstractLimiterThe complete Colella & Woodward (1984) PPM, for PPMScheme only.
Three changes from PPMScheme(MonotoneLimiter()):
Edge values use van Leer-limited slopes (CW84 eqs. 1.7–1.8),
with , so each edge lies between its two neighbouring cell means. In smooth regions this is the fourth-order edge of the default. The profile is limited with the same monotonicity conditions (eq. 1.10).
The face flux integrates the donor parabola over the swept fraction (eq. 1.12) instead of using the outflow edge as a linear slope.
Every limited parabola lies between neighbouring cell means, so a sweep keeps a non-negative tracer non-negative as long as no cell exports more than its air mass (the fractions swept out through its two faces sum to at most 1). In uniform one-dimensional flow with Courant number up to 1 the scheme is monotone. The stencil is the same six cells as the default, so halo widths do not change.
sourceAtmosTransport.Operators.Advection.FV3ScalarProfile Type
FV3ScalarProfile(; positive_definite = true) <: AbstractVerticalReconstructionThe vertical profile of FV3's tracer remapping, scalar_profile with kord = 8 (fv_mapz.F90), which GEOS-Chem High Performance uses for tracers (kord_tr = 8, positive definite, iv = 0). With positive_definite = false it is FV3's profile for signed fields (iv = 1): the three non-negativity steps below are omitted and the profile is symmetric under q → −q, except where neighbouring layer means are exactly equal (FV3 resolves such ties with its local-minimum branch whatever the sign). Use it for tracers that become negative, such as flux anomalies; the positive-definite profile flattens layers with a non-positive mean. Each layer carries the parabola
q(s) = q_L + s [(q_R − q_L) + q_6 (1 − s)], 0 ≤ s ≤ 1 from layer top to bottom,whose mean is the layer mean. The edge values q_L, q_R solve FV3's compact tridiagonal system, weighted by layer air mass, and are then limited: large-scale constraints at every edge (non-negative at local minima), Huynh's second constraint and positivity in layers 3 … Nz−2, the standard PPM monotonicity limiter in layers 2 and Nz−1, and a monotone, non-negative profile in the top and bottom layers.
The flux through an interface is the air-mass flux times the mean of the donor layer's parabola over the swept fraction α = |F| / m_donor (Courant number ≤ 1): the bottom fraction of the layer above for downward flow, the top fraction of the layer below for upward flow. Each interface flux is evaluated once and enters both layers with opposite signs, so only the rounding of the cell updates changes the column tracer mass, in every precision.
Available on cubed-sphere grids. See docs/src/theory/vertical_transport.md.
AtmosTransport.Operators.Advection.LinRoodPPMScheme Type
LinRoodPPMScheme{ORD, Z} <: AbstractAdvectionScheme
LinRoodPPMScheme(ORD = 5; vertical = UpwindScheme())Cubed-sphere Lin-Rood / FV3-style cross-term PPM advection with compile-time edge-value family ORD (not a global spatial or temporal accuracy order).
This is distinct from PPMScheme: PPMScheme participates in the standard Strang split implemented by strang_split_cs!, while LinRoodPPMScheme selects the FV3-style horizontal Lin-Rood update (fv_tp_2d_cs!). Its vertical sweeps use vertical: first-order UpwindScheme() (default) or PPMScheme(; vertical = FV3ScalarProfile()), FV3's kord = 8 profile (TOML vertical = "fv3_kord8"). The low-level Julia constructor retains its API default of upwind; the TOML runtime defaults configured Lin–Rood runs to fv3_kord8 for the closest GCHP match.
Supported orders currently match the implemented PPM edge-value families in ppm_subgrid_distributions.jl:
ORD = 5— Colella–Woodward fourth-order edge interpolation with extremum flatteningORD = 7— ORD=5 interior with special cubed-sphere face treatment
Examples
LinRoodPPMScheme() # default ORD=5, upwind vertical
LinRoodPPMScheme(7) # ORD=7 cubed-sphere boundary treatment
LinRoodPPMScheme(7; vertical = PPMScheme(; vertical = FV3ScalarProfile()))AtmosTransport.Operators.Advection.MonotoneLimiter Type
MonotoneLimiter <: AbstractLimiterVan Leer minmod limiter (van Leer 1977; Sweby 1984).
Limits the centered slope against the two one-sided differences scaled by 2, using the three-argument minmod function:
This is TVD (total variation diminishing) and preserves monotonicity. TM5 does not use this limiter: its slopes advection carries the slope as a prognostic moment and only clamps it to
AtmosTransport.Operators.Advection.NoAdvection Type
NoAdvection <: AbstractAdvectionSchemeIdentity scheme — apply! is a no-op. Default for runs that need to isolate other operators (convection, chemistry, diagnostics) from the advective transport block, e.g. a "convection-alone" timing experiment or a synthetic-state regression.
When NoAdvection is selected:
The advection sweeps are skipped on all three topologies (
LatLonMesh,ReducedGaussianMesh,CubedSphereMesh).Diffusion is allowed: with no advection to wrap, the V(dt) diffusion step is applied directly through the mass-flux VMR kernels, which are mass-conserving on their own and need no Strang palindrome. This is the natural "diffusion-only" experimental setup.
Surface emissions are rejected with an actionable
ArgumentError. They are integrated as Strang half-steps wrapping the advection block, so running them without advection silently drops 2nd-order accuracy. To run a convection-only configuration omit[tracers.*.surface_flux]blocks.The CS workspace is auto-allocated by the
TransportModelconstructor when diffusion is active (and isnothingwhen diffusion is also off).
Example
scheme = NoAdvection()AtmosTransport.Operators.Advection.NoBinaryCFLCheck Type
NoBinaryCFLCheck()Trust the binary's substep schedule (the default). The preprocessor already checks it when it writes the binary.
sourceAtmosTransport.Operators.Advection.NoLimiter Type
NoLimiter <: AbstractLimiterNo limiting applied. The slope is the full centered difference
AtmosTransport.Operators.Advection.PPMScheme Type
PPMScheme{L <: AbstractLimiter, V <: AbstractVerticalReconstruction} <: AbstractQuadraticSchemePiecewise Parabolic Method (Colella & Woodward 1984; Putman & Lin 2007).
Reconstructs a parabolic subcell profile constrained by the cell mean and limited edge values. With the default limiter, in every sweep that uses the shared face flux, the limited edge only sets a linear slope toward the outflow face (Russell–Lerner form), so the update is second order with a PPM-informed slope; it is not monotone. With CW84Limiter the edges are van Leer-limited and the flux integrates the parabola over the swept fraction: the complete Colella–Woodward scheme, which keeps non-negative tracers non-negative in every sweep in which no cell exports more than its air mass.
Implemented for structured latitude-longitude and cubed-sphere grids. The TOML runner selects the monotone variant with scheme = "ppm" and the complete CW84 scheme with limiter = "cw84"; it does not accept ppm_order for this scheme. Reduced-Gaussian face-indexed transport does not support PPM.
Kernel tests and real-input V100 conservation/performance experiments cover this path. These do not establish full-model TM5/GCHP parity or positivity of the complete cubed-sphere update: small negative column means have been observed.
Fields
limiter::L— parabolic profile limiting policy (default:MonotoneLimiter();CW84Limiter()for the complete Colella–Woodward scheme)vertical::V— vertical reconstruction:SameAsHorizontal(default) orFV3ScalarProfile(cubed sphere; TOMLvertical = "fv3_kord8", or"fv3_kord8_signed"for signed tracers)
Example
PPMScheme() # monotone-limited PPM
PPMScheme(CW84Limiter()) # complete CW84 PPM
PPMScheme(NoLimiter()) # unlimited (may oscillate)
PPMScheme(; vertical = FV3ScalarProfile()) # FV3 kord = 8 vertical profileAtmosTransport.Operators.Advection.PositivityLimiter Type
PositivityLimiter <: AbstractLimiterLimits the slope to keep the reconstructed face values non-negative: the centered slope keeps its sign and is clipped to
Weaker than MonotoneLimiter (it does not bound new maxima) but sufficient for species that must remain positive (e.g., tracer mixing ratios). Requires a non-negative tracer; not for signed (anomaly) tracers.
AtmosTransport.Operators.Advection.SameAsHorizontal Type
SameAsHorizontal <: AbstractVerticalReconstructionThe vertical sweep uses the same face flux as the horizontal sweeps: a uniform-index fourth-order edge value, limited with the Colella–Woodward (1984) conditions, sets a linear slope toward the outflow face, and the flux takes the Russell–Lerner (1981) form. The parabola is never integrated, so the vertical sweep is second order. The top two and bottom two layers use upwind fluxes. This is the default.
sourceAtmosTransport.Operators.Advection.SlopesScheme Type
SlopesScheme{L <: AbstractLimiter} <: AbstractLinearSchemeVan Leer slopes advection with the Russell–Lerner face flux (van Leer 1977; Russell & Lerner 1981).
Reconstructs a piecewise-linear subcell profile in each cell using a limited slope, then integrates the Courant-fraction swept volume to compute the face flux. Second-order accurate in smooth regions.
The slope is re-diagnosed from the neighbouring cell means at every sweep; the scheme carries no moment state. This is not TM5's slopes scheme. TM5 uses the same face flux, but carries the first moments (rxm, rym, rzm) as prognostic tracer state with their own update equations and limits them only for positivity (MonotoneLimiter minmod rule, the TM5 update reproduces SlopesScheme() to rounding (test/core/test_slopes_tm5_reference.jl); with the prognostic moment, TM5 is markedly less diffusive. A prognostic-moment scheme is not implemented (see Advection schemes).
The face tracer flux for positive mass flux
where
See _slopes_face_flux in reconstruction.jl for the full derivation and _limited_slope in limiters.jl for the limiter implementations.
Fields
limiter::L— slope/moment limiting policy (default:MonotoneLimiter())
Examples
SlopesScheme() # monotone-limited (default)
SlopesScheme(NoLimiter()) # unlimited (2nd order, may oscillate)
SlopesScheme(PositivityLimiter()) # positivity-preservingAtmosTransport.Operators.Advection.TracerView Type
TracerView{FT, A}Lightweight wrapper around a 4D array (Nx, Ny, Nz, Nt) that presents it as a 3D array (Nx, Ny, Nz) for a fixed tracer index t.
This enables the multi-tracer kernels to call the existing @inline face flux functions (which expect 3D rm arrays) without any code duplication. Julia's compiler inlines the getindex call and eliminates the wrapper entirely — zero overhead on both CPU and GPU.
Example
rm_4d = zeros(Float64, 36, 18, 4, 50) # Nx, Ny, Nz, Nt
rm_t = TracerView(rm_4d, Int32(3)) # view of tracer 3
rm_t[10, 5, 2] # equivalent to rm_4d[10, 5, 2, 3]AtmosTransport.Operators.Advection.UpwindScheme Type
UpwindScheme <: AbstractConstantSchemeFirst-order donor-cell (Godunov) upwind scheme.
The tracer flux through a face is simply the mass flux times the mixing ratio of the upstream cell:
where
Properties: conservative, monotone, first-order accurate. Strongly diffusive but useful as a reference and for positivity-critical applications.
Example
scheme = UpwindScheme()AtmosTransport.Operators.Advection.apply_divergence_damping_cs! Method
apply_divergence_damping_cs!(rm_panels, m_panels, mesh, ws, damp_coeff)Conservative del-2 divergence damping on tracer panels. Mass-conserving flux-form Laplacian diffusion on mixing ratio (c = rm/m). Typical damp_coeff values: 0.02-0.05 for mild panel-boundary noise.
AtmosTransport.Operators.Advection.apply_linrood_update_adjoint! Method
apply_linrood_update_adjoint!(lambda_rm, lambda_m,
lambda_fx_in, lambda_fx_out,
lambda_fy_in, lambda_fy_out,
lambda_rm_new, lambda_m_new,
am, bm, mesh)Apply the discrete transpose of _linrood_update_kernel! for one panel: accumulate the adjoint of (rm_new, m_new) into the adjoint inputs (rm, m, fx_in, fx_out, fy_in, fy_out) for fixed velocities (am, bm).
All lambda_* adjoint accumulators are read-and-modified (atomically for face arrays) — callers are responsible for initialising them to zero before the call. The kernel only touches interior (i, j) indices 1..Nc; halo cells of lambda_rm/lambda_m and face cells outside 1..Nc+1 are left untouched.
AtmosTransport.Operators.Advection.apply_ppm_x_face_adjoint! Method
apply_ppm_x_face_adjoint!(lambda_rm, lambda_m, lambda_fx_face, rm, m, am,
mesh, ::Val{ORD})Discrete transpose of _ppm_x_face_kernel! (LinRood.jl:270) at ORD=5 or ORD=7. Folds _safe_mixing_ratio into the d6-AD chain and includes the donor-mass α = F / m_donor contribution. Atomic accumulation on shared cells.
ORD=7 uses the per-face cube-edge values of _ppm_edge_values_ord7 (seam value at faces 1 and Nc + 1, FV3 one-sided values at faces 0, 2, Nc and Nc + 2) and recomputes the donor α-contribution against the same limited (q_L, q_R). Faces whose stencil is clear of those faces are bit-equal to ORD=5.
AtmosTransport.Operators.Advection.apply_ppm_x_face_from_q_adjoint! Method
apply_ppm_x_face_from_q_adjoint!(lambda_q, lambda_fx_face, q, am, m,
mesh, ::Val{ORD})Discrete transpose of _ppm_x_face_from_q_kernel! (LinRood.jl:299) for one panel at ORD=5 or ORD=7 (LinRoodPPMScheme). The donor-mass denominator m_l/m_r in _ppm_face_value and the velocity am are treated as fixed parameters from the tape — the adjoint propagates only the q-stencil sensitivity. Atomic writes on lambda_q because multiple faces share each cell.
ORD=7 dispatches to a kernel that evaluates the forward per-face cube-edge values (_ppm_edge_values_ord7) on dual numbers before the monotonicity step; faces whose stencil is clear of the faces at and next to the panel edges are bit-equal to ORD=5.
AtmosTransport.Operators.Advection.apply_ppm_y_face_adjoint! Method
apply_ppm_y_face_adjoint!(lambda_rm, lambda_m, lambda_fy_face, rm, m, bm,
mesh, ::Val{ORD})Discrete transpose of _ppm_y_face_kernel! (LinRood.jl:241) at ORD=5 or ORD=7. See apply_ppm_x_face_adjoint! for the contract.
AtmosTransport.Operators.Advection.apply_ppm_y_face_from_q_adjoint! Method
apply_ppm_y_face_from_q_adjoint!(lambda_q, lambda_fy_face, q, bm, m,
mesh, ::Val{ORD})Discrete transpose of _ppm_y_face_from_q_kernel! (LinRood.jl:325) for one panel at ORD=5 or ORD=7. See apply_ppm_x_face_from_q_adjoint! for the contract.
AtmosTransport.Operators.Advection.apply_pre_advect_x_adjoint! Method
apply_pre_advect_x_adjoint!(lambda_rm, lambda_m, lambda_fx_face,
lambda_q_j, rm, m, am, fx_face, mesh)Discrete transpose of _pre_advect_x_kernel! for one panel. See apply_pre_advect_y_adjoint! for the contract — same structure with X substituted for Y.
AtmosTransport.Operators.Advection.apply_pre_advect_y_adjoint! Method
apply_pre_advect_y_adjoint!(lambda_rm, lambda_m, lambda_fy_face,
lambda_q_i, rm, m, bm, fy_face, mesh)Discrete transpose of _pre_advect_y_kernel! for one panel: accumulate lambda_q_i into the adjoint accumulators of (rm, m, fy_face) for fixed velocity bm. The small-m_new zeroing exactly mirrors _safe_mixing_ratio (the _linrood_mass_floor threshold, 100·eps).
All lambda_* accumulators are read-modified; callers initialise them to zero before the call. Face writes use @atomic for shared neighbour-cell accumulation along Y.
AtmosTransport.Operators.Advection.copy_corners! Method
copy_corners!(panels, mesh, dir)Standalone corner fill for 6-panel fields. Rotates corner halo cells according to sweep direction dir (1 = X-sweep, 2 = Y-sweep); with Hp > 0 any other dir throws an ArgumentError.
AtmosTransport.Operators.Advection.fill_panel_halos! Method
fill_panel_halos!(panels::NTuple{6}, mesh::CubedSphereMesh; dir=0, sync=true)Fill halo regions of a 6-panel cubed-sphere field by copying interior data from neighboring panels with correct edge-to-edge orientation mapping.
Each panels[p] must be (Nc + 2Hp) × (Nc + 2Hp) × ... (3-D fields or packed (x, y, z, tracer) storage) with interior at [Hp+1:Hp+Nc, Hp+1:Hp+Nc, ...].
If dir is 1 or 2, corner fill is performed for the given sweep direction (1=X, 2=Y) using the FV3 tp_core rotation formulas.
On a GPU with the default fusion policy (CUDA) this is one launch for all panel edges, a second for the corners when dir is 1 or 2, and one synchronization; on Metal the 24 edges and 6 corner blocks are launched one by one with their panel arrays bound, then synchronized once. With sync = false a device fill is only queued (later work on the same queue sees its result).
AtmosTransport.Operators.Advection.fv_tp_2d_cs! Method
fv_tp_2d_cs!(rm_panels, m_panels, am_panels, bm_panels,
mesh, ::Val{ORD}, ws, ws_lr; damp_coeff=0.0)Lin-Rood horizontal advection for cubed-sphere grids. Averages X-first and Y-first PPM orderings, then shares the final seam mixing-ratio estimates between neighboring panels before their flux updates. The transverse formulation follows FV3; the explicit seam projection is AtmosTransport's conservative interface coupling.
recorder observes the phases (record_linrood_horizontal!); the production NoStepRecorder() makes every hook a no-op. The cubed-sphere adjoint builds its _CSLinRoodHorizRecord from these hooks.
AtmosTransport.Operators.Advection.fv_tp_2d_cs_q! Method
fv_tp_2d_cs_q!(q_panels, m_panels, am_panels, bm_panels,
mesh, ::Val{ORD}, ws, ws_lr; damp_coeff=0.0)Q-space Lin-Rood horizontal advection. Evolves q (mixing ratio) and m (air mass, same role as pressure thickness) in-place. m_panels is both read (for CFL fraction in PPM face values) and written (mass divergence update).
AtmosTransport.Operators.Advection.reconstruction_order Method
reconstruction_order(scheme) → IntReturn the polynomial order of the subcell reconstruction:
0 for constant (upwind)
1 for linear (slopes)
2 for quadratic (PPM)
Useful for diagnostics and for selecting stencil widths in multi-tracer kernel fusion.
sourceAtmosTransport.Operators.Advection.required_halo_width Method
required_halo_width(scheme) -> IntReturn the minimum cubed-sphere halo width needed by scheme's horizontal stencil. This is a capability query, not a reconstruction-order query: several schemes can share the same polynomial family while using different CS execution paths.
AtmosTransport.Operators.Advection.strang_split! Method
strang_split!(state, fluxes, grid, scheme; workspace)Perform one full Strang-split advection step on a structured mesh.
Splitting sequence
X → Y → Z → Z → Y → X
─────────────────────────
half half full half halfWhen the palindrome outflow budget needs n > 1 subcycles, the step runs (X → Y → Z)^n → midpoint → (Z → Y → X)^n with every flux divided by n (see strang_split_mt!).
All Nt = ntracers(state) tracers are advanced together in a single multi-tracer kernel launch per direction. The mass update is computed once per cell; tracer fluxes are evaluated per-tracer inside the kernel. The per-tracer Julia loop has been eliminated in favour of strang_split_mt! on the packed state.tracers_raw buffer.
Arguments
state::CellState— containsair_massandtracers_rawfluxes::StructuredFaceFluxState— mass fluxes (am, bm, cm)grid::AtmosGrid{<:LatLonMesh}— structured lat-lon gridscheme— advection scheme (AbstractAdvectionScheme)workspace::AdvectionWorkspace— pre-allocated double buffers; useAdvectionWorkspace(state)so the 4D ping-pong buffers are sized forntracers(state).
AtmosTransport.Operators.Advection.strang_split_cs! Method
strang_split_cs!(panels_rm, panels_m, panels_am, panels_bm, panels_cm,
mesh, scheme, workspace; flux_scale=1,
cfl_limit=DEFAULT_CFL_LIMIT)Perform one Strang-split advection step on a 6-panel cubed-sphere field with automatic CFL-based subcycling.
Splitting sequence
With n subcycles and every flux divided by n:
repeat n times: ← forward half, (X Y Z)^n
[fill_panel_halos!(dir=1)] ← from the second pass on: Z changed the interiors
X sweep
→ fill_panel_halos!(dir=1) ← exchange halos between panels (X direction)
→ Y sweep
→ fill_panel_halos!(dir=2) ← exchange halos between panels (Y direction)
→ Z sweep
midpoint!() ← diffusion / surface flux, once
repeat n times: ← reverse half, (Z Y X)^n
Z sweep
→ fill_panel_halos!(dir=2)
→ Y sweep
→ fill_panel_halos!(dir=2)
→ fill_panel_halos!(dir=1)
→ X sweep
→ fill_panel_halos!(dir=1)The first X sweep reads the halos as the caller leaves them: production strang_split! and the adjoint replays refresh them with fill_cs_step_start_halos! at every step start.
At n = 1 this is Strang's X-Y-Z-Z-Y-X palindrome. For n > 1 the composition stays symmetric (the reverse half is the adjoint of the forward half), so it keeps the palindrome's second-order splitting with the same 6n sweeps. Second-order splitting requires sufficiently accurate subflows; the composition alone does not prove the transport's temporal order. Horizontal groups pair physical seam transfers before halo exchange supplies neighboring values for the next reconstruction.
Subcycling
The count comes from the palindrome budget 2 * (out_x + out_y + out_z) / (n min(m_start, m_end)) ≤ cfl_limit, m_end the cell's mass after the step (_cs_static_palindrome_subcycle_count). A supplied subcycle_count uses the binary's schedule; a workspace built with binary_cfl_check = BinaryCFLCheck() ([advection] check_binary_cfl = true) checks it against the runtime budget.
Panel array layout
Each panel's rm and m arrays are (Nc+2Hp, Nc+2Hp, Nz) with Hp-wide halos. Interior cells are at indices [Hp+1:Hp+Nc, Hp+1:Hp+Nc, :]. The sweep kernels only update interior cells; halo regions are filled by fill_panel_halos! from adjacent panels.
Arguments
panels_rm,panels_m: NTuple{6} of 3D arrays(Nc+2Hp, Nc+2Hp, Nz)— tracer mass and air mass. Modified in-place.panels_am,panels_bm,panels_cm: NTuple{6} of flux arrays.am[Nc+2Hp+1, Nc+2Hp, Nz],bm[Nc+2Hp, Nc+2Hp+1, Nz],cm[Nc+2Hp, Nc+2Hp, Nz+1]. Read-only.mesh:CubedSphereMeshwith Nc, Hp, and panel connectivity.scheme: advection scheme —UpwindScheme()uses gamma-clamped upwind for tracer face transfers.SlopesScheme()andPPMScheme()use the generic KA kernels with_xface_tracer_fluxdispatch. Higher-order schemes requiremesh.Hp ≥ 2(Slopes) ormesh.Hp ≥ 3(PPM).workspace: pre-allocatedCSAdvectionWorkspacebuffers.flux_scale: overall scaling applied to all fluxes (default 1.0).cfl_limit: palindrome outflow budget per subcycle, as a fraction of the smaller of the cell's masses at the start and end of the step (defaultDEFAULT_CFL_LIMIT, 0.95).
AtmosTransport.Operators.Advection.strang_split_linrood_ppm! Method
strang_split_linrood_ppm!(rm_panels, m_panels, am_panels, bm_panels, cm_panels,
mesh, ::Val{ORD}, ws, ws_lr; cfl_limit=DEFAULT_CFL_LIMIT,
damp_coeff=0.0)Full 3D advection: Horizontal(LR) → Z → Z → Horizontal(LR).
sourceAtmosTransport.Operators.Advection.strang_split_mt! Method
strang_split_mt!(rm_4d, m, am, bm, cm, scheme, ws)Multi-tracer Strang-split advection on a packed 4D tracer array.
This is the performance-optimized path: all Nt = size(rm_4d, 4) tracers are processed in a SINGLE kernel launch per sweep direction (6 total), rather than 6 × Nt launches in the per-tracer path.
The mass update (m_new = m + flux_in - flux_out) is computed ONCE per cell per sweep, shared across all tracers.
Subcycling
One count n comes from the palindrome outflow budget 2 (out_x + out_y + out_z) / (n min(m, m_end)) ≤ cfl_limit (m_end the cell's mass after the step) that the cubed sphere uses (_structured_palindrome_subcycle_count, default cfl_limit = DEFAULT_CFL_LIMIT). The step then runs the symmetric composition
(X Y Z)^n → midpoint operators → (Z Y X)^n, every pass with flux / n,6n sweeps, second order, and exactly X Y Z | midpoint | Z Y X at n = 1.
Arguments
rm_4d::AbstractArray{FT,4}— tracer mass(Nx, Ny, Nz, Nt), mutatedm::AbstractArray{FT,3}— air mass(Nx, Ny, Nz), mutatedam, bm, cm— mass fluxes (x, y, z directions)scheme::AbstractAdvectionScheme— advection schemews::AdvectionWorkspace— workspace with 4D buffer allocated
Convection
AtmosTransport.Operators.Convection Module
ConvectionConvective transport operators.
This submodule ships the convection-operator hierarchy:
AbstractConvection— supertype for all convective transport operators.NoConvection— identity no-op; default for configurations without active convection. Dispatch is a compile-time dead branch inTransportModel.step!so the presence of the convection block has zero cost for users who don't opt in.
Operators:
ConvectionForcingin..MetDriverscarries the convective forcing arrays (withcopy_convection_forcing!,allocate_convection_forcing_like, and window-struct integration).CMFMCConvection(GCHP path, CMFMC+DTRAIN kernel with mandatory CFL sub-cycling and well-mixed sub-cloud).TM5Convection(TM5 column solver) withTM5Workspace, running on all three topologies (LL / RG / CS).
The step!-level runtime block wires convection across all three topologies (LL / RG / CS).
apply! contract
apply!(state::CellState{B},
forcing::ConvectionForcing,
grid::AtmosGrid,
op::AbstractConvection,
dt::Real;
workspace) where {B <: AbstractMassBasis}The operator takes ConvectionForcing directly (not a transport window or driver). _refresh_forcing! populates model.convection_forcing at the first substep of each window by copying from sim.window.convection. TransportModel.step! executes the convection block between transport and chemistry. No meteo kwarg — the forcing arrays are the time information; the operator does not call current_time.
Face-indexed scope
CMFMCConvection now supports structured LatLon, face-indexed ReducedGaussian, and panel-native CubedSphere state layouts. The CS path keeps forcing panel-native too: the driver loads cmfmc / dtrain as per-panel tuples and the operator applies the same column-local logic on each panel interior.
TM5Convection ships the same three-topology scope.
The no-op NoConvection path accepts any state shape — it's a pure dead branch.
AtmosTransport.Operators.Convection.AbstractCloudBase Type
CMFMCEdgeCloudBase()
ArchivedCloudBase()Where CMFMCConvection puts the convective cloud base, below which the column is mixed before the updraft starts. CMFMCEdgeCloudBase (default) takes the lowest layer with updraft inflow through its bottom edge. ArchivedCloudBase takes it from the met forcing (forcing.cloud_base): GEOS-Chem's lowest layer with convective rain production DQRCU > 0 (convection_mod.F90, DO_RAS_CLOUD_CONVECTION). Cubed sphere only.
AtmosTransport.Operators.Convection.CMFMCConvection Type
CMFMCConvection(; clamp=false, cloud_base=CMFMCEdgeCloudBase())Explicit, conservative convection driven by per-layer convective mass flux and detrainment from ConvectionForcing. Supported on lat-lon, reduced-Gaussian, and cubed-sphere states. Setting clamp=true applies the positivity correction and conservative column rescaling used for strong-CFL forcing. cloud_base selects the cloud-base rule (see AbstractCloudBase).
AtmosTransport.Operators.Convection.CMFMCMatrixConvection Type
CMFMCMatrixConvection(; tile_workspace_gib = 1.0,
use_collab_lu = false,
lmax_conv = 0,
n_merge = 1)GEOS-derived rates through the TM5 LU solver. The kwarg-tuned knobs all pass through to the inner TM5Convection that performs the column solve — see its docstring for tile_workspace_gib, use_collab_lu, lmax_conv, and n_merge semantics.
Forcing requirements
forcing.cmfmc and forcing.dtrain must be populated on the GEOS binary's basis (whatever state.air_mass is — dry for our production runtime). forcing.tm5_fields is ignored. The operator runs once-per-met-window derivation to convert (cmfmc, dtrain) → (entu, detu, 0, 0), caching the result in CMFMCMatrixWorkspace for reuse across all substeps.
Conservation contract
Σ(m·q) is preserved to floating-point roundoff for any inert tracer: the backward-Euler matrix has unit column sums once Σ entu = Σ detu, which the derivation kernels guarantee by absorbing any boundary residual at the surface layer. The solve therefore preserves the sum of its right-hand side up to rounding.
Adjoint
The derivation is constant in the state vector — it depends only on forcings — so the state-space adjoint is exactly the inner TM5 LU adjoint applied to the cached derived rates. See Adjoints/ConvectionAdjoint.jl.
AtmosTransport.Operators.Convection.CMFMCMatrixWorkspace Type
CMFMCMatrixWorkspace{FT, TS, EU, ED, V}Per-sim pre-allocated workspace for CMFMCMatrixConvection.
Fields
tm5_workspace :: TS— the underlyingTM5Workspacecarrying the column-tile matrix slab, pivots, and TM5 cell-area metrics.derived_entu :: EU— derived updraft entrainment rate, same shape and basis asforcing.dtrain(LL:(Nx,Ny,Nz), RG:(ncells,Nz), CS:NTuple{6, (Nc,Nc,Nz)}). Refreshed on every met-window advance viainvalidate_cmfmc_matrix_cache!; reused bit-exact across all substeps within a window.derived_detu :: EU— derived updraft detrainment rate, same shape asderived_entu. Includes the explicit GEOSdtrainplus any negative-entrainment residual folded in to keepentu ≥ 0.zero_entd :: ED— immutable zeros, same shape asderived_entu. Passed as the TM5 downdraft entrainment field (CMFMC has no downdraft in our binary contract).zero_detd :: ED— immutable zeros, same shape (TM5 downdraft detrainment).derived_valid :: V—Base.RefValue{Bool}flagging whether the cached derivation matches the current met window. Cleared viainvalidate_cmfmc_matrix_cache!on window advance.
Lifecycle
The derived rates are computed once per met-window from forcing.cmfmc + forcing.dtrain by _launch_cmfmc_matrix_derivation! (see cmfmc_matrix_kernels.jl). Within a window, apply! reuses the cached arrays directly — the LU solve sees the same (entu, detu, 0, 0) quadruplet every substep.
AtmosTransport.Operators.Convection.CMFMCMatrixWorkspace Method
CMFMCMatrixWorkspace(air_mass; tile_workspace_gib = nothing,
tile_columns = nothing,
cell_metrics = nothing,
halo_width = 0) -> CMFMCMatrixWorkspaceConstruct a fresh workspace from an air-mass payload. air_mass may be a single array (LL / RG) or a panel tuple (CS). The inner TM5Workspace is built with the same tile-budget rules as a bare TM5Workspace; the derived-rate slabs are sized to mirror the layout of forcing.dtrain (LL/RG: same as air_mass; CS: halo-stripped from air_mass).
cell_metrics is the topology cell-area metric (mandatory in production — the TM5 matrix needs it to convert kg-per-cell air mass to the kg/m² basis the convective fluxes share).
halo_width is the per-panel halo half-width Hp for cubed-sphere payloads (the production CS factory passes grid.horizontal.Hp). LL and RG leave it at 0.
AtmosTransport.Operators.Convection.CMFMCWorkspace Type
CMFMCWorkspace{FT, QC, CA, L}Per-sim pre-allocated workspace for CMFMCConvection.
Fields
qc_scratch :: QC— updraft concentration buffer, one entry per cell, shape matchingair_mass((Nx, Ny, Nz)for structured grids,(ncells, Nz)for face-indexed grids). Reused across all substeps and all tracers.cell_metrics :: CA— pre-adapted cell-area metric vector used by the CFL scan and the kernel:cell_areas_by_latitude(mesh)for structured grids, per-cellcell_area(mesh, c)for face-indexed grids.cached_n_sub :: Base.RefValue{Int}— the CFL-derived sub-step count for the current met window. Stays valid as long ascache_valid[] == true; re-computed from the current CMFMC / air-mass state on the nextapply!call when invalid. Int (not Float) because it's the integer sub-step count.cache_valid :: Base.RefValue{Bool}— sentinel; cleared viainvalidate_cmfmc_cache!when the met window advances (DrivenSimulation._maybe_advance_window!).ledger_carry :: Base.RefValue{L}— persistent state of the column mass ledger, as inTM5Workspace: per column and tracer, the part of the last update's rounding residual that the column's largest layer could not hold. Empty until the firstapply!sizes it with zeros; not stored in restarts.
Usage
Constructed once at DrivenSimulation setup time and reused for the whole run. Adapt.adapt_structure preserves the cached scalar state on the host while adapting qc_scratch to the requested backend.
AtmosTransport.Operators.Convection.NoConvection Type
NoConvection()Identity operator — apply! is a no-op. Default for configurations without active convection. Dispatch is a compile-time dead branch in TransportModel.step!, so the convection block collapses to zero floating-point work when no operator is installed (bit-exact backward- compatible with the no-op behavior).
AtmosTransport.Operators.Convection.TM5Convection Type
TM5Convection(; tile_workspace_gib=1.0, use_collab_lu=false,
lmax_conv=0, n_merge=1)TM5-style convective transport operator. Four-field mass-flux scheme following Tiedtke (1989) as implemented in TM5-4DVAR: two entrainment and two detrainment fields (updraft + downdraft). The backward-Euler transport matrix conv1 = I - dt·D is dense within the cloud window and identity above; the solver assembles and factorizes only the active lower-right cloud block. The legacy workspace retains its pivot vector for supported adjoint replay; collaborative factors live only in shared memory.
use_collab_lu selects the workgroup-collaborative solver. lmax_conv=0 uses all vertical levels; a positive value limits the active lower atmosphere. n_merge optionally aggregates adjacent active layers before the solve (see Level merging below). Float32 GPU matrices support effective depths 1..85. Float64 CUDA supports unmerged (n_merge=1) depths 1..73, using Float64 shared arrays and arithmetic. CPU and unsupported Float64 configurations retain the legacy solver with a warning. Tracers use batches of six; total tracer count does not set this depth limit. Defaults remain the full-column legacy solve. CS adjoint footprints still require the full-column, unmerged legacy variant.
The forcing arrays (entu, detu, entd, detd) arrive via TransportModel.convection_forcing.tm5_fields, populated at the first substep of each window by DrivenSimulation._refresh_forcing! from sim.window.convection.
Level merging (n_merge > 1)
An AtmosTransport approximation; TM5 itself merges levels once, in its met preprocessing, and never reconstructs a fine profile. Each group s of n_merge adjacent fine layers k becomes one super-layer with tracer mass R_s = Σ_k r_k, air mass M_s = Σ_k m_k, and summed forcings. After the backward-Euler solve on the super grid gives R'_s, each fine layer receives a share of the super-layer's change:
r'_k = r_k + (R'_s - R_s) * w_k, w_k = |r_k| / Σ_{j∈s} |r_j|with w_k = m_k / M_s if Σ_{j∈s} |r_j| = 0, and w_k = 1 / n_merge if M_s = 0 as well.
The weights lie in [0, 1] and sum to one. The split conserves the column total, keeps a uniform mixing ratio, leaves columns and layers without convection unchanged bit for bit, and changes no cell by more than |R'_s - R_s|. If a super-layer's fine values share one sign, it equals the proportional split r'_k = R'_s (r_k / R_s) and is evaluated in that form, so non-negative tracers stay non-negative in floating point as well. A column mass ledger over the whole merged operation returns the split's rounding to the column (see the ledger_carry of TM5Workspace). Only super-layers holding values of both signs can increase the column's Σ|r|: by at most 2 Σ_{s mixed} |R'_s - R_s| in total. The split is not linear in the tracer; merged solves have no adjoint (CS footprints require n_merge = 1).
Memory budget
tile_workspace_gib (binary GiB) is the per-topology target for the TM5 column-tile workspace. _convection_workspace_for(::TM5Convection, ...) reads this field and derives a tile size B via derive_tile_columns; the TM5Workspace uses a single (Nz, Nz, B) matrix slab plus matching pivot, cloud-dimension, and flux-vector slabs. A larger budget means fewer legacy kernel launches at the cost of more memory. When collaborative LU is requested, global scratch is deferred because the kernels use shared memory. Unsupported configurations allocate the configured tile on first use and reuse it. The default legacy budget is 1.0 GiB.
Basis convention
TM5Convection is basis-polymorphic, identical to CMFMCConvection. The four forcing fields must be on the same basis as state.air_mass. ECMWF and TM5 archive convection fields are total-air (moist) fluxes upstream; maintained preprocessors convert them to the binary's declared basis before writing and record that basis as tm5_convection_mass_basis. Opening a binary rejects a recorded mismatch. Legacy dry binaries without that key warn because their unconverted moist fluxes make rates about 1 % too large.
Fields required on ConvectionForcing
forcing.tm5_fields :: NamedTuple{(:entu, :detu, :entd, :detd)}with all four arrays at layer centers in AtmosTransport orientation (k=1=TOA, k=Nz=surface). Units kg / m² / s. Shapes per topology:Structured LatLon:
(Nx, Ny, Nz)per field.Face-indexed ReducedGaussian:
(ncells, Nz)per field.Panel-native CubedSphere:
NTuple{6, AbstractArray{FT, 3}}per field, with per-panel shape(Nc, Nc, Nz).
Orientation conversion + sign flip on entd happen in the preprocessor (src/Preprocessing/tm5_convection_conversion.jl). The operator performs zero runtime orientation gymnastics.
Solver class
Partial-pivot Gaussian elimination on the active lower-right block per column (see test/core/test_tm5_sparsity_above_icltop.jl for the structure survey). Identity rows above the effective cloud top and the lower-left zero quadrant are skipped by both the factorization and the tracer solve.
Pivoting is kept even though the matrix is diagonally dominant by construction (upstream Fortran comment says pivoting "not needed"). The pivot vector is stored in TM5Workspace so the adjoint can replay the same factorization with trans='T'.
CFL sub-cycling
None. The backward-Euler matrix solve is unconditionally stable for any dt, unlike CMFMCConvection's forward-Euler two-pass update which requires sub-cycling when the CMFMC profile is strong. The kernel launches once per tile and calls synchronize(backend) once per apply!.
AtmosTransport.Operators.Convection.TM5Workspace Type
TM5Workspace{FT, M, P, C, F, A, CA, L}Per-sim pre-allocated workspace for TM5Convection.
Fields
conv1 :: M—conv1 = I - dt·Dmatrix slab, one(Nz, Nz)block per column. Parametric on array type soAdapt.adapt_structurecan swap CPU ↔ GPU without changing theTM5Workspacetype constructor. Shapes per topology:Structured LatLon:
(Nz, Nz, Nx, Ny)— 4D.Face-indexed ReducedGaussian:
(Nz, Nz, ncells)— 3D.Panel-native CubedSphere:
NTuple{6, AbstractArray{FT, 4}}with per-panel shape(Nz, Nz, Nc, Nc).
pivots :: P— permutation vector from partial-pivot LU, one Nz-length Int slice per column. Preserved so the adjoint can replay the same factorization with transposed back-substitution. Shapes strip the leading Nz fromconv1shape:(Nz, Nx, Ny)/(Nz, ncells)/NTuple{6, (Nz, Nc, Nc)}.cloud_dims :: C— per-column(icltop, iclbas, icllfs)triple in AtmosTransport indexing (k=1=TOA, k=Nz=surface; the preprocessor delivers forcings in this orientation so the solver has zero orientation logic). Shape(3, Nx, Ny)/(3, ncells)/NTuple{6, (3, Nc, Nc)}.f_scratch :: F— per-column intermediate matrix for the matrix build (TM5f(0:lmx, 1:lmx), with the updraft merged intof). Pre-allocated so_tm5_build_conv1!runs without heap allocation inside KA kernels (mandatory on GPU; same contract on CPU for parity).f_scratchaliasesconv1in the production workspace becauseconv1is only needed afterfhas been converted intoI - dt*D; this saves one dense(Nz, Nz)slab per column. There is no standalonefu_scratchfield — the updraft and downdraft passes write disjoint index ranges, so the builder writes directly intof.amu_scratch :: A,amd_scratch :: A— length-(Nz+1)per-column boundary-aware mass-flux vectors (TM5amu(0:lmx)/amd(0:lmx)). Same allocation policy as the scratch matrices.cell_metrics :: CA— pre-adapted cell-area metrics used to convert runtimeair_massfrom kg per grid cell to the kg/m² column mass expected by the TM5 matrix.nothingis allowed only for standalone unit-area column tests; topology-levelapply!requires this field to be populated.ledger_carry :: L— persistent state of the column mass ledger: per column and tracer, the part of the last solve's rounding residual that the column's largest cell could not hold (about half its ulp at most; see_ledger_deposit). Shape(Nx, Ny, Nt)/(ncells, Nt)/NTuple{6, (Nc, Nc, Nt)}. The constructor allocates it empty, with the topology's storage type; the firstapply!sizes it with zeros, and a call with another tracer layout starts from zero again. Restarts do not store it: a restarted run drops about half an ulp of each column's largest cell, once.
Usage
Constructed once at DrivenSimulation setup time via _convection_workspace_for(::TM5Convection, state, grid). Adapt.adapt_structure adapts each array to the requested backend without reallocating on the host.
AtmosTransport.Operators.Convection.TM5Workspace Method
TM5Workspace(air_mass; tile_columns = …,
tile_workspace_gib = nothing,
cell_metrics = nothing,
defer_scratch = false) -> TM5WorkspaceConstruct a fresh workspace from an air-mass payload. air_mass may be a single array (structured (Nx, Ny, Nz) or face-indexed (ncells, Nz)) or a cubed-sphere panel tuple — the workspace itself is topology-agnostic. The per-column element type of conv1 matches eltype(air_mass).
The per-launch column count B is set by exactly one of:
tile_columns::Integer(explicit). Default_tm5_total_cells_per_launch(air_mass)— one tile covers the whole launch and the workspace is bit-equal to the earlier per-cell allocator. Production paths use this branch when they already knowB.tile_workspace_gib::Real(budget). PicksBviaderive_tile_columnsfromTM5Convection'stile_workspace_gibfield. Bypassed iftile_columnsis also passed (explicit wins).cell_metricscarries topology cell areas. Production_convection_workspace_for(::TM5Convection, ...)supplies this; leaving itnothingis only for unit-area solver tests.
Specifying both tile settings is an error. defer_scratch=true retains the configured tile size but allocates no legacy matrix payload until a legacy solve actually needs it. Adaptation drops this recomputable scratch while preserving cell metrics; each backend allocates its own tile on demand.
AtmosTransport.Operators.Convection.apply_convection! Method
apply_convection!(q_raw, air_mass, forcing::ConvectionForcing,
::NoConvection, dt, workspace, grid) -> nothingArray-level no-op, parallels the the diffusion and surface-flux apply! pattern. Accepts any shape of q_raw / air_mass — NoConvection doesn't inspect them. Returns nothing.
The structured apply! flow goes through the state-level method above. apply_convection! is reserved for the future case where the convection block is called from inside a palindrome or another composed setting — same signature contract as the diffusion and surface-flux array entry points.
AtmosTransport.Operators.Convection.apply_convection! Method
apply_convection!(q_raw, air_mass, forcing::ConvectionForcing,
op::TM5Convection, dt, workspace::TM5Workspace,
grid::AtmosGrid) -> nothingArray-level entry point — parallels the CMFMC contract at operators.jl:70-89. Dispatches on grid mesh type and launches the matching KA kernel from tm5_kernels.jl. Single synchronize(backend) at the end (TM5 matrix solve is unconditionally stable; no sub-cycling).
AtmosTransport.Operators.Convection.derive_tile_columns Method
derive_tile_columns(::Type{FT}, Nz, budget_gib, total_cells) -> IntPick a tile size B from a per-cell memory cost and a target budget. The cost model accounts for every per-cell field that TM5Workspace tiles:
conv1:Nz²×sizeof(FT)amu_scratch+amd_scratch:2(Nz+1)×sizeof(FT)pivots:Nz×sizeof(Int)cloud_dims:3×sizeof(Int)
f_scratch is a structural alias for conv1 (saves one matrix slab per cell). B is clamped between 256 (avoid pathological launches) and total_cells (one tile covers the whole topology when the budget allows it).
AtmosTransport.Operators.Convection.invalidate_cmfmc_cache! Method
invalidate_cmfmc_cache!(ws::CMFMCWorkspace) -> nothingMark the cached CFL n_sub as stale. Called on met-window advance by DrivenSimulation._maybe_advance_window!. The next apply! recomputes n_sub from the fresh CMFMC array.
AtmosTransport.Operators.Convection.invalidate_cmfmc_matrix_cache! Method
invalidate_cmfmc_matrix_cache!(ws::CMFMCMatrixWorkspace) -> nothing
invalidate_cmfmc_matrix_cache!(::Any) -> nothingMark the derived (entu, detu) cache stale so the next apply! re-derives from forcing.cmfmc + forcing.dtrain. Called on met-window advance from DrivenSimulation._maybe_advance_window! via the shared invalidate_cmfmc_cache! dispatch.
Diffusion
AtmosTransport.Operators.Diffusion Module
DiffusionVertical-diffusion operator hierarchy and solver infrastructure.
Public surface:
NoDiffusion— identity no-op; the default when no[diffusion]config section is present.ImplicitVerticalDiffusion— Backward-Euler implicit diffusion driven by anAbstractTimeVaryingFieldKz. Wired into the Strang palindrome viaapply_vertical_diffusion_vmr!and installed intoTransportModel.diffusionby the runtime recipe when[diffusion] kind = "constant".
Both subtype the global AbstractDiffusion declared in src/Operators/AbstractOperators.jl; concrete operator structs live in operators.jl. The column-level Thomas solve (solve_tridiagonal!) is exposed as the numerical reference. Generic Kz kernels name the tridiagonal coefficients (a, b, c) explicitly. Precomputed Dkg mass transport uses column-conservative bidiagonal factors in conservative_dkg.jl; both matching transposes live in src/Adjoints/DiffusionAdjoint.jl.
AtmosTransport.Operators.Diffusion.AbstractSurfaceFluxCoupling Type
AbstractSurfaceFluxCouplingTyped policy for where configured surface fluxes enter relative to a vertical diffusion/mixing operator.
SplitSurfaceFluxCoupling: existing Strang-center composition,V(dt/2) -> S(dt) -> V(dt/2).DiffusiveSurfaceFluxBoundary: GCHP/VDIFF-style lower-boundary placement,S(dt) -> V(dt), so fresh surface flux is included in the implicit vertical mixing solve.
AtmosTransport.Operators.Diffusion.DiffusiveSurfaceFluxBoundary Type
DiffusiveSurfaceFluxBoundary()Inject surface mass immediately before one full implicit vertical diffusion solve, so the new surface mass participates in that solve.
sourceAtmosTransport.Operators.Diffusion.ImplicitVerticalDiffusion Type
ImplicitVerticalDiffusion(; kz_field)Backward-Euler vertical diffusion driven by either a cell-centered Kz field or an exact precomputed TM5 interface dkg field. Two spatial layouts are supported:
structured:
AbstractTimeVaryingField{FT, 3}over(Nx, Ny, Nz)face-indexed:
AbstractTimeVaryingField{FT, 2}over(ncells, Nz)
Concrete examples:
ConstantField{FT, 3}/ConstantField{FT, 2}ProfileKzField{FT}with default rank 3 orProfileKzField(profile; spatial_rank = 2)PreComputedKzField{FT, A}wrapping 3D or 2D storageDerivedKzFieldfor meteorology-driven Beljaars-Viterbo on structured gridsCubedSphereFieldwrapping one structured rank-3 field per panel
apply! contract
apply!(state, meteo, grid, op::ImplicitVerticalDiffusion, dt; workspace)Refreshes the Kz cache with
update_field!(op.kz_field, current_time(meteo));meteo = nothingusest = 0for standalone calls.For Kz fields, reads
workspace.layer_thicknessas the current layer thicknesses [m]. The caller is responsible for filling this array before callingapply!— typically from a hydrostatic integration of the currentdelpand surface temperature.PrecomputedCSDkgFieldbypasses Kz/geometry reconstruction and does not readlayer_thickness; its interface exchange [kg s⁻¹] is already complete.Uses
workspace.factorsfor Thomas elimination or Dkg mass-retention factors. The Dkg mass path does not use the packed reference scratch.Launches a topology-specific mass-flux kernel. Packed layouts factor each atmospheric column once and advance every tracer with those factors.
The spatial operator is linear, but Backward Euler is not a semigroup: V(dt) and V(dt/2) ∘ V(dt/2) differ by O(dt²). The surface-coupling policy therefore selects the timestep composition explicitly.
Fields
kz_field::KzF— anyAbstractTimeVaryingField{FT, 2}orAbstractTimeVaryingField{FT, 3}providing cell-centered Kz values [m²/s geometric], or aPrecomputedCSDkgField.
AtmosTransport.Operators.Diffusion.NoDiffusion Type
NoDiffusion()Identity operator — apply! is a no-op. Default for configurations without active vertical mixing, and the value strang_split_mt! sees when the palindrome's V position is unoccupied.
AtmosTransport.Operators.Diffusion.SplitSurfaceFluxCoupling Type
SplitSurfaceFluxCoupling()Apply surface flux at the transport-palindrome center, bracketed by two half-step diffusion solves.
sourceAtmosTransport.Operators.Diffusion.apply_vertical_diffusion_vmr! Function
apply_vertical_diffusion_vmr!(rm, air_mass, op, workspace, dt, meteo = nothing;
[halo_width, tracer]) -> nothingApply one Backward-Euler diffusion step to tracer mass rm (in and out): structured (Nx, Ny, Nz, Nt), face-indexed (ncells, Nz, Nt) or (ncells, Nz), or six halo-padded cubed-sphere panels (Nc + 2Hp, Nc + 2Hp, Nz[, Nt]) with halo_width = Hp. This is the entry point of the transport palindrome and of the cubed-sphere adjoint replays. For Kz fields the column solve gives the new mixing ratio and the tracer mass is updated in flux form, rm_k += F_{k−½} − F_{k+½}, so each column total changes only by unbiased rounding (diffusion_kernels.jl). Precomputed cubed-sphere dkg fields use the conservative mass solve of conservative_dkg.jl.
NoDiffusion is a no-op method, so call sites compile to nothing.
AtmosTransport.Operators.Diffusion.apply_vertical_diffusion_vmr! Method
apply_vertical_diffusion_vmr!(rm, air_mass, op, workspace, dt, meteo;
halo_width, tracer = nothing)Cubed-sphere helper for state variables stored as tracer mass. The implicit solver uses the current dry air mass. Precomputed Dkg uses conservative bidiagonal factors directly on tracer mass. Kz fields solve for the mixing ratio and update the tracer mass in flux form before advection resumes.
The Dkg solve keeps a column mass ledger whose sub-ulp remainder persists in workspace.ledger_carry, one value per column and tracer (see _ledger_deposit). Packed tracers ((N, N, Nz, Nt) panels) use carry slot t for tracer t. A single-tracer panel tuple uses slot tracer, its index in the state; with tracer = nothing it keeps no carry, so the call is a function of its input alone (adjoint tapes and replays rely on this). Other fields ignore tracer.
AtmosTransport.Operators.Diffusion.fill_dz_hydrostatic_constT! Method
fill_dz_hydrostatic_constT!(dz, ps, ak_ifc, bk_ifc;
T_ref = HYDROSTATIC_REFERENCE_TEMPERATURE,
R = R_DRY_AIR, gravity = STANDARD_GRAVITY)Populate a 3D (Nx, Ny, Nz) dz array (host or device) from surface pressure ps and hybrid sigma-pressure interface coefficients ak_ifc, bk_ifc (length Nz + 1). Backend follows dz.
AtmosTransport.Operators.Diffusion.fill_dz_hydrostatic_constT! Method
fill_dz_hydrostatic_constT!(dz::AbstractArray{<:Any, 2},
ps::AbstractArray{<:Any, 1},
ak_ifc, bk_ifc; ...)Face-indexed variant for ReducedGaussian topology: dz shape (ncells, Nz), ps shape (ncells,). Same constant-T_ref formula as the structured/CS overloads, just unrolled over the face-indexed cell axis.
AtmosTransport.Operators.Diffusion.fill_dz_hydrostatic_constT! Method
fill_dz_hydrostatic_constT!(dz_panels::NTuple{6}, ps_panels::NTuple{6},
ak_ifc, bk_ifc; ...)Cubed-sphere variant: per-panel 3D (Nc, Nc, Nz) dz arrays are filled from per-panel ps arrays of shape (Nc, Nc) (interior only — the runtime stores surface_pressure without the advection halo).
AtmosTransport.Operators.Diffusion.fill_dz_hydrostatic_virtualT! Method
fill_dz_hydrostatic_virtualT!(dz, t_lyr, qv_lyr, ps, ak_ifc, bk_ifc;
R = R_DRY_AIR, gravity = STANDARD_GRAVITY)Populate a 3D (Nx, Ny, Nz) dz array using virtual temperature per layer: T_v = T · (1 + VIRTUAL_TEMPERATURE_FACTOR · qv) (factor 0.61). Matches the geometry the LocalHoltslagBovilleKzField uses for its column-mid heights, closing the inconsistency between solver dz and Kz-cache dz.
t_lyr, qv_lyr are layer-center 3D fields with shape (Nx, Ny, Nz), typically pulled from the active window's VDIFF payload.
AtmosTransport.Operators.Diffusion.fill_dz_hydrostatic_virtualT! Method
fill_dz_hydrostatic_virtualT!(dz_panels::NTuple{6}, t_panels::NTuple{6},
qv_panels::NTuple{6}, ps_panels::NTuple{6},
ak_ifc, bk_ifc; ...)Cubed-sphere variant of fill_dz_hydrostatic_virtualT!. Per-panel 3D (Nc, Nc, Nz) dz/t_lyr/qv_lyr arrays + per-panel (Nc, Nc) ps. The VDIFF payload is panel-native, so passing the panel tuples directly is the natural runtime shape.
AtmosTransport.Operators.Diffusion.recover_total_surface_pressure! Method
recover_total_surface_pressure!(dest, ps_dry, qv, A, B)Recover total surface pressure on the array backend used by dest. For the hybrid interfaces p_{k+1/2}=A_{k+1/2}+B_{k+1/2}p_{s,total}, stored dry surface pressure satisfies
The implementation uses KernelAbstractions for CPU and the package's supported CUDA and Metal backends; device arrays are never scalar-indexed by the host. For an A-only coordinate (B ≡ 0) recovery is unobservable and unnecessary, so ps_dry is copied unchanged. Otherwise the humidity-weighted B slope must be finite and positive or the function throws ArgumentError.
AtmosTransport.Operators.Diffusion.solve_tridiagonal! Method
solve_tridiagonal!(x, a, b, c, d, w)Solve the tridiagonal linear system T x = d in place, where T has sub-diagonal a, main diagonal b, super-diagonal c. By convention a[1] and c[Nz] are ignored (no-neighbor positions).
Argument roles:
x::AbstractVector{FT}— output, overwritten with the solution.a, b, c, d::AbstractVector{FT}— read only, not mutated.w::AbstractVector{FT}— caller-supplied workspace, length ≥ Nz. Holds the Thomas forward-elimination factorsw[k] = c[k] / denomused during back-substitution.
Implements the standard Thomas algorithm with one extra array (w) so that b and d can be read-only. For per-column Nz this is Θ(Nz) arithmetic, no allocation.
Adjoint note (this routine is forward-only)
The matrix transpose T^T is obtained by swapping shifted sub- and super-diagonals:
a_T[k] = c[k - 1] # for k ≥ 2
b_T[k] = b[k]
c_T[k] = a[k + 1] # for k ≤ Nz - 1The CS adjoint kernel _vertical_diffusion_cs_single_adjoint_kernel! in src/Adjoints/DiffusionAdjoint.jl builds (a_T, b_T, c_T) inline and runs an inlined Thomas (rather than reusing this routine) so the kernel stays allocation-free.
SurfaceFlux
Source containers, the surface-flux operator, and the emission totals (total_emission_rate, emission_segments, integrated_emission) used by logs and budget diagnostics. Emission totals gives the equations.
AtmosTransport.Operators.SurfaceFlux Module
SurfaceFluxSurface emission operators.
Ships the data types and helpers needed to apply per-tracer surface sources to a CellState:
SurfaceFluxSource{RateT}— single-tracer source plus a rate array in model-storage units per second per cell, also available from the top-levelAtmosTransportmodule. Physical inventories in kg species/s are converted to these units by the runtime's basis-aware input builders.PerTracerFluxMap{S}— NTuple-backed map ofSurfaceFluxSources, keyed bytracer_name. Ships with theflux_for(map, :name)lookup helper. Storage-bits-stable on GPU.
The AbstractSurfaceFluxOperator hierarchy (NoSurfaceFlux, SurfaceFluxOperator), the _surface_flux_kernel! KA kernel, and the apply! / apply_surface_flux! entry points live alongside these data types.
A TimeVaryingSurfaceFluxSource keeps its rate series in host memory. Its device copy is a small window buffer (FluxWindowBuffer) holding only the slices that the current met window reads; stage_surface_flux_window! fills it when a window becomes current, and the kernels read the rates from it.
Emission totals for logs and budgets come from the operator's own temporal reconstruction: total_emission_rate(src, t) (global storage rate at an instant), emission_segments(src, t, dt) (the pieces one step applies) and integrated_emission(src, t0, t1, dt) (the mass added over a run's steps).
Surface layer convention
All kernels here assume k = Nz is the surface. This matches src/Models/DrivenSimulation.jl and the LatLon grid storage layout. A future AbstractLayerOrdering{TopDown, BottomUp} refactor can generalise this.
AtmosTransport.Operators.SurfaceFlux.AbstractFluxTemporalScheme Type
AbstractFluxTemporalSchemeHow a TimeVaryingSurfaceFluxSource reconstructs a per-step rate from its stored time slices at t = current_time(meteo) (and step dt). Concrete schemes implement
_flux_temporal_weights(scheme, times, t, dt) -> (i0, i1, w0, w1)returning two bracketing slice indices and blend weights (w0 + w1 == 1) applied as w0·series[i0] + w1·series[i1], together with
_flux_temporal_segments(scheme, times, t, dt) -> [(i0, i1, w0, w1, frac), …]
_slices_read_at(scheme, times, τ) -> (first, last)the pieces of one step [t, t + dt], and the first and last slice index that an evaluation at the instant τ reads (non-decreasing in τ; the device window buffer of a time-varying source holds the slices between those of a window's start and end). New reconstructions (e.g. mass-flux-integral matching, higher-order in time) are added as new subtypes and these methods — no kernel or operator change.
AtmosTransport.Operators.SurfaceFlux.AbstractSurfaceFluxOperator Type
AbstractSurfaceFluxOperatorTop of the surface emission operator hierarchy. Concrete subtypes:
NoSurfaceFlux— identity (default).SurfaceFluxOperator— wraps aPerTracerFluxMapand applies each tracer's surface flux tok = Nzof the conservative tracer-storage array.
Every concrete subtype implements two entry points:
State-level
apply!(state, meteo, grid, op, dt; workspace)→ mutatesstate.tracers_raw.Array-level
apply_surface_flux!(q_raw, op, ws, dt, meteo, grid; tracer_names)→ mutates a raw tracer buffer directly. Supported layouts are:structured packed:
(Nx, Ny, Nz, Nt)face-indexed packed:
(ncells, Nz, Nt)face-indexed single-tracer slice:
(ncells, Nz)cubed-sphere single-tracer panels:
NTuple{6}of(Nc + 2Hp, Nc + 2Hp, Nz)Used by the structured multi-tracer palindrome and the reduced-Gaussian face-indexed transport block.
AtmosTransport.Operators.SurfaceFlux.AbstractSurfaceFluxSource Type
AbstractSurfaceFluxSourceSupertype for all per-tracer surface emission sources consumed by the surface-flux operator and stored in a PerTracerFluxMap.
Concrete subtypes:
SurfaceFluxSource— a single static per-cell rate array.TimeVaryingSurfaceFluxSource— a time series of per-cell rate slices plus a sortedtimesvector; the operator linearly interpolates to the simulation clock atapply!time.
Every subtype carries a tracer_name :: Symbol field used by flux_for to key the map.
AtmosTransport.Operators.SurfaceFlux.ConservativeMeanFlux Type
ConservativeMeanFlux()Window/step integral-conserving: evaluate the linear reconstruction at the step centre t + dt/2. Summed over the substeps of a met window, this midpoint quadrature reproduces the exact trapezoidal time-integral of a piecewise-linear flux — i.e. the emitted mass per window matches the source's own integral, independent of the substep count.
AtmosTransport.Operators.SurfaceFlux.LinearInterpFlux Type
LinearInterpFlux()Linear interpolation between the two bracketing slices, point-evaluated at the step's current_time(meteo). HEMCO-like treatment of instantaneous 3-hourly fields. Default scheme.
AtmosTransport.Operators.SurfaceFlux.NoSurfaceFlux Type
NoSurfaceFlux()Identity operator — apply! is a no-op. Default for configurations without surface emissions, and the value strang_split_mt! sees when the palindrome's S position is unoccupied.
NoSurfaceFlux's apply! is literally = state (and the array-level apply_surface_flux! is = nothing). Julia's dispatch turns the call site into a dead branch with zero floating-point work and a bit-exact no-op.
AtmosTransport.Operators.SurfaceFlux.PerTracerFluxMap Type
PerTracerFluxMap{S <: Tuple}An ordered tuple of SurfaceFluxSources, keyed by the tracer_name field on each entry. Supplies the surface flux operator with per-tracer surface-rate data and ensures efficient tuple-splatting at kernel-launch time.
The map is NTuple-backed rather than Dict-backed. Rationale:
Runtime source construction naturally produces a small tuple; the map keeps that storage and adds a lookup helper.
Tuples are bits-stable on GPU (captured by kernels without boxing / hashing); Dicts would require special Adapt handling and per-launch lookup cost.
Small-N (typically 1-5 emitting tracers) makes linear scan cheaper than hashing anyway.
Tracers absent from the map have zero surface flux. The consumer operator iterates the map and applies each source; it does NOT walk the state's full tracer_names and look up each by name.
Construction
co2_rate = fill(2.0e-7, Nx, Ny) # model-storage units/s per cell
sf6_rate = fill(1.5e-9, Nx, Ny) # already converted from physical flux
rn222_rate = fill(3.0e-11, Nx, Ny)
map = PerTracerFluxMap(
SurfaceFluxSource(:CO2, co2_rate),
SurfaceFluxSource(:SF6, sf6_rate),
SurfaceFluxSource(:Rn222, rn222_rate),
)
length(map) # 3
flux_for(map, :CO2) === ... # the :CO2 SurfaceFluxSource
flux_for(map, :CH4) # nothing (not emitting)Adapt / GPU
Adapt.adapt(CuArray, map) adapts each source through its adapt_structure; tuples adapt element-wise. A SurfaceFluxSource moves its cell_mass_rate to the device. A TimeVaryingSurfaceFluxSource moves only its compensation and its small window buffer; its rate series stays in host memory.
Fields
sources :: S—NTuple{N, <:AbstractSurfaceFluxSource}for someN. Each entry carries a tracer name and its rate data (a static array forSurfaceFluxSource, a time series forTimeVaryingSurfaceFluxSource).
AtmosTransport.Operators.SurfaceFlux.PerTracerFluxMap Method
PerTracerFluxMap(sources::AbstractVector{<:AbstractSurfaceFluxSource})
PerTracerFluxMap(sources::Tuple)Generic collection constructor. The input is frozen into an NTuple.
sourceAtmosTransport.Operators.SurfaceFlux.PerTracerFluxMap Method
PerTracerFluxMap(sources::AbstractSurfaceFluxSource...)Variadic constructor: PerTracerFluxMap(src1, src2, src3) wraps the three sources into an NTuple-backed map.
AtmosTransport.Operators.SurfaceFlux.StepwiseFlux Type
StepwiseFlux()Piecewise-constant in time: emit the value of the slice block containing t (the largest slice time ≤ t), with no blending. This holds each 3-hourly CAMS value constant across its block — the "change the flux in 3h blocks" option.
AtmosTransport.Operators.SurfaceFlux.SurfaceFluxOperator Type
SurfaceFluxOperator(flux_map::PerTracerFluxMap)
SurfaceFluxOperator(sources::SurfaceFluxSource...)Applies a PerTracerFluxMap of surface sources to the k = Nz slab of the conservative tracer-storage array.
For every tracer named in the flux map, the operator launches the layout-appropriate surface-flux kernel and adds rate × dt to the surface layer k = Nz of the matching tracer:
structured packed state
(Nx, Ny, Nz, Nt)→_surface_flux_kernel!over(Nx, Ny)face-indexed packed state
(ncells, Nz, Nt)→_surface_flux_face_kernel!overncells
Tracer indices are resolved on the host from state.tracer_names. Tracers absent from the map are untouched.
The rate is a model-storage amount per second per cell (already area-integrated); no cell-area multiplier appears in the kernel. File-based physical kg-species rates are converted by the source builder before they reach this operator.
apply! contract
apply!(state, meteo, grid, op::SurfaceFluxOperator, dt; workspace)Walks
op.flux_mapin storage order.For each source, skips tracers not present in
state.Dispatches to the array-level entry point with
tracer_namespulled from the state.Synchronises the backend once at the end.
Fields
flux_map :: M— aPerTracerFluxMapofSurfaceFluxSources. Emitting tracers are exactly those named in the map.
AtmosTransport.Operators.SurfaceFlux.SurfaceFluxOperator Method
SurfaceFluxOperator(sources::SurfaceFluxSource...)Convenience constructor: wraps a variadic list of sources in a PerTracerFluxMap first. Empty variadic list is allowed (produces an empty-map operator; equivalent to NoSurfaceFlux except type- distinguishable).
AtmosTransport.Operators.SurfaceFlux.SurfaceFluxSource Type
SurfaceFluxSource{RateT}A single-tracer surface source: a tracer_name plus a cell_mass_rate array supplying model-storage amount added per cell per second to the surface layer.
tracer_name :: Symbol— matches a name inCellState.tracer_names.cell_mass_rate :: RateT— one of:a 2D
(Nx, Ny)array for structured gridsa 1D
(Nc,)array for face-indexed gridsan
NTuple{6}of 2D(Nc, Nc)arrays for cubed-sphere panels
The rates are already area-integrated. For dry-VMR tracers, file-based physical fluxes in kg species/s are converted by the source builder to dry-air-equivalent storage units before reaching this struct. The surface flux kernel applies
rm_surface += rate × dtwithout multiplying by cell area.
Why per-cell rates (not kg/m²/s)
The prognostic tracer is stored per cell, so a per-cell rate × dt is the natural unit. A per-area variant would need to multiply by cell area before entering the operator.
Provenance
The source lives with SurfaceFluxOperator; model and simulation layers only compose and schedule the operator.
Fields
tracer_name :: Symbolcell_mass_rate :: RateT— backend-agnostic;Adapt.adaptconverts the array between host and device transparently viaAdapt.adapt_structure.
AtmosTransport.Operators.SurfaceFlux.SurfaceLayerDeposit Type
SurfaceLayerDeposit()
ProfileDeposit(profile)Vertical placement of the mass emitted into a cubed-sphere column during one step. SurfaceLayerDeposit adds it to the surface layer k = Nz with Kahan compensation. ProfileDeposit spreads it over the column with per-layer fractions profile[i, j, k] that sum to one: the non-local PBL transport of fresh surface emissions in GEOS-Chem's VDIFF (the counter-gradient term ∂(ρ K γ)/∂z, linear in the surface flux). A fraction may be negative: that layer loses tracer to the layers above.
Of a column's emission qmincg check, the column keeps all of
with npbl layers, which hold every nonzero
Both placements add exactly _two_sum, and the surface layer receives the emission minus what landed above through the Kahan update, so the column gains exactly the emitted mass.
AtmosTransport.Operators.SurfaceFlux.TimeVaryingSurfaceFluxSource Type
TimeVaryingSurfaceFluxSource{RateT, T, S, CompT, BufT}A single-tracer surface source whose per-cell rate advances through a time series of slices with the simulation clock. Used to carry the CAMS/LMDZ 3-hourly diurnal cycle (instead of collapsing it to a monthly mean), the daily and hourly cs_native biosphere drivers, and to match GeosChem.
tracer_name :: Symbol— matches a name inCellState.tracer_names.cell_mass_rate_series :: RateT— a stack of static rate slices, in the same per-cell model-storage units asSurfaceFluxSource. For cubed-sphere this is anNTuple{6}of(Nc, Nc, ntime)arrays (one stacked panel per face). The trailing dimension is time. The series stays where it was built (host memory for every loader):Adaptdoes not move it, so a multi-year series costs no device memory.times :: T— slice times in seconds since run start, lengthntime, sorted strictly ascending. Stays a hostVectoreven after Adapt-to-device so the operator cansearchsortedlastit on the host before launching the interpolated kernel.scheme :: S— anAbstractFluxTemporalSchemeselecting how the stored slices are reconstructed onto the simulation clock (StepwiseFlux,LinearInterpFlux, orConservativeMeanFlux). A singleton, carried through Adapt unchanged.compensation :: CompT— Kahan compensation, one slice in shape, on the compute backend.window_buffer :: BufT— aFluxWindowBuffer: the slices the current met window reads, on the compute backend.Adaptmoves it (andcompensation) to the device.
The operator resolves the segments (i0, i1, w0, w1, frac) of _flux_temporal_segments(scheme, times, current_time(meteo), dt) on the host, makes their slices resident in window_buffer (the runner has staged the window's slices before its first step, see stage_surface_flux_window!), then blends the two slices in the kernel. End slices are clamped (constant extrapolation) outside [times[1], times[end]].
AtmosTransport.Operators.SurfaceFlux.apply_surface_flux! Function
apply_surface_flux!(q_raw, op::AbstractSurfaceFluxOperator, workspace, dt, meteo, grid; tracer_names)
Array-level surface-flux application. Writes directly to the supplied tracer buffer q_raw, adding each source's rate × dt contribution to the surface slab k = Nz.
Supported layouts:
structured packed:
q_raw :: (Nx, Ny, Nz, Nt)face-indexed packed:
q_raw :: (ncells, Nz, Nt)face-indexed single-tracer slice:
q_raw :: (ncells, Nz)
tracer_names::NTuple{Nt, Symbol} is required as a keyword so the function can resolve each source's name to a slab index without reaching back into the caller's CellState. This lets the palindrome integration point the operator at either the caller's state.tracers_raw or the workspace's ping-pong buffer — whichever currently holds the post-Z-sweep tracer state.
workspace, meteo, and grid are accepted but currently unused; they are in the signature to match the operator-interface convention (§"Workflow: Adding a new physics operator" in CLAUDE.md) and to leave room for future extensions (e.g. meteorology-dependent emissions).
Returns nothing on success. For NoSurfaceFlux, returns nothing immediately (zero floating-point work).
AtmosTransport.Operators.SurfaceFlux.apply_surface_flux! Method
apply_surface_flux!(q_raw::NTuple{6}, op, workspace, dt, meteo, grid;
tracer_names, halo_width, deposit = SurfaceLayerDeposit())Cubed-sphere surface fluxes on halo-padded panels, single-tracer (Nc + 2Hp, Nc + 2Hp, Nz) (exactly one name in tracer_names) or packed (…, Nz, Nt). deposit places each column's emitted mass: SurfaceLayerDeposit() or an NTuple{6} of per-panel ProfileDeposits.
AtmosTransport.Operators.SurfaceFlux.emission_deposit Method
emission_deposit(diffusion_op) -> SurfaceLayerDeposit() or NTuple{6, ProfileDeposit}Where the diffusion scheme puts fresh surface emissions: the surface layer, except for GEOS-Chem's non-local VDIFF, whose counter-gradient transport spreads them over the boundary layer (GCHPNonlocalPBLField.emission_profile).
AtmosTransport.Operators.SurfaceFlux.emission_segments Function
emission_segments(src::AbstractSurfaceFluxSource, t, dt) -> Vector{Tuple{Float64, Float64}}The pieces in which one surface-flux step [t, t + dt] adds src: pairs (R, τ) of global storage rate (kg s⁻¹) and duration (s) with apply_surface_flux! adds cell by cell.
SurfaceFluxSource: one piece(R, dt).TimeVaryingSurfaceFluxSource: one piece per segment of_flux_temporal_segments(scheme, times, t, dt).LinearInterpFluxgives one piece, the rate interpolated at the step startt.StepwiseFluxandConservativeMeanFluxsplit the step at every slice time inside it and evaluate each piece at its midpoint. This integrates the piecewise-constant or piecewise-linear reconstruction exactly, so their pieces over one long step and over many short steps give the same integral.
Within a piece the rate is constant (StepwiseFlux, LinearInterpFlux) or is the piece mean of a linear reconstruction (ConservativeMeanFlux). Budget diagnostics that integrate a sink alongside the source (radioactive decay over a met window, for example) need these pieces rather than only their sum.
AtmosTransport.Operators.SurfaceFlux.emitting_tracer_indices Method
emitting_tracer_indices(op::SurfaceFluxOperator, state::CellState) -> NTupleOrdered tuple of tracer indices in state.tracer_names for the tracers present in op.flux_map. Tracers in the map but missing from the state are skipped (returned as nothing slots). Useful for testing / introspection and unchanged by apply!.
AtmosTransport.Operators.SurfaceFlux.flux_for Method
flux_for(map, tracer_name::Symbol) -> SurfaceFluxSource | nothingReturn the SurfaceFluxSource for the named tracer, or nothing if the tracer has no surface source in this map. O(N) linear scan, which is fine for typical N ≤ 10.
AtmosTransport.Operators.SurfaceFlux.flux_temporal_scheme Method
flux_temporal_scheme(name) -> AbstractFluxTemporalSchemeMap a config string to a temporal scheme. "stepwise"/"block" → StepwiseFlux; "linear"/"interp" → LinearInterpFlux; "conservative"/"window_mean"/"integral" → ConservativeMeanFlux.
AtmosTransport.Operators.SurfaceFlux.integrated_emission Function
integrated_emission(src::AbstractSurfaceFluxSource, t0, t1, dt) -> Float64Global storage mass (kg dry-air equivalent) that the surface-flux operator adds from src over the run's steps of length dt between t0 and t1 (seconds since the run start): the steps start at
the pieces of emission_segments. A SurfaceFluxSource gives
For StepwiseFlux and ConservativeMeanFlux, E is the exact integral of the reconstruction over [t0, t1] and does not depend on dt (up to rounding); one step of length t1 - t0 gives the same value. For LinearInterpFlux, E is the step-start Riemann sum of the interpolated rate and depends on dt: pass the run's advection step.
Slice totals are summed once per call in Float64 on the host series (as for total_emission_rate, a series built on a device is copied one slice at a time) and the step sum is compensated (Neumaier), so a run-long integral carries about one rounding of the total. Throws ArgumentError unless dt, t0 and t1 are finite, dt > 0, t1 ≥ t0, and (t1 - t0) / dt is an integer to a relative √eps(Float64).
AtmosTransport.Operators.SurfaceFlux.total_emission_rate Function
total_emission_rate(src::AbstractSurfaceFluxSource, t) -> Float64Global emission rate of src at the instant t (seconds since the run start): the per-cell model-storage rates summed over all cells, in kg dry-air equivalent s⁻¹ (physical kg species s⁻¹ × DRY_AIR_MOLAR_MASS / M_species).
SurfaceFluxSource:, independent of t.TimeVaryingSurfaceFluxSource: the scheme's reconstruction att,with and (i₀, i₁, w₀, w₁) = _flux_temporal_weights(scheme, times, t, 0). That is the slice in effect (largest slice time ≤t) forStepwiseFluxand linear interpolation between the bracketing slices forLinearInterpFluxandConservativeMeanFlux. Outside[times[1], times[end]]the end slices hold.
Cell sums are accumulated in Float64 (pairwise) whatever the storage float type, so a Float32 source gives its total to Float64 accuracy. The sums run on the host, because Metal has no Float64: a static source adapted to a device (CUDA, Metal) is copied to host memory first. A time-varying source keeps its series in host memory on every backend (only its window buffer is on the device), so it is summed in place; a series built directly on a device is copied one slice at a time. The runner logs total_emission_rate(source, 0.0), the rate at the run start.
Chemistry
AtmosTransport.Operators.Chemistry Module
ChemistrySource/sink operators for tracer transformations (decay, photolysis, ...).
Type hierarchy:
AbstractChemistryOperator
├── NoChemistry — identity / inert tracers
├── ExponentialDecay{FT, N} — multi-tracer first-order decay
└── CompositeChemistry — sequential compositionInterface (OPERATOR_COMPOSITION.md §6):
apply!(state::CellState, meteo, grid, op::AbstractChemistryOperator, dt;
workspace=nothing)The operator mutates state.tracers_raw in place and returns state. meteo, grid, and workspace are accepted for interface conformance and may be nothing for operators that do not need them (pure decay).
Multi-tracer decay is fused into a single KernelAbstractions kernel — see chemistry_kernels.jl. Tracers not listed in the operator's tracer_names are left untouched.
AtmosTransport.Operators.Chemistry.CompositeChemistry Type
CompositeChemistry(schemes...)
CompositeChemistry(schemes::Tuple)Apply multiple chemistry operators sequentially. Used when different species need independent transformations or when different operator types (decay + photolysis + ...) must run in a prescribed order.
chem = CompositeChemistry(
ExponentialDecay(; Rn222 = 330_350.4),
ExponentialDecay(; Kr85 = 3.394e8),
)AtmosTransport.Operators.Chemistry.ExponentialDecay Type
Keyword constructor: ExponentialDecay(; Rn222 = half_life_seconds, ...).
AtmosTransport.Operators.Chemistry.ExponentialDecay Type
ExponentialDecay{FT, N, R}(decay_rates, tracer_names)Multi-tracer first-order decay: c *= exp(-rate * dt) applied in-place to every selected tracer at every cell. Exact for constant rate and any dt; unconditionally stable; trivially parallel.
Fields
decay_rates :: R— anNTuple{N, <: AbstractTimeVaryingField{FT, 0}}of rate-valued fields, one per selected tracer [1/s].apply!callsupdate_field!on each rate before launching the kernel.tracer_names :: NTuple{N, Symbol}— which tracers this operator applies to
Construction
ExponentialDecay(; Rn222 = 330_350.4) # from half-lives [s]
ExponentialDecay(Float32; Rn222 = 330_350.4, Kr85 = 3.394e8)The keyword constructor converts half-life T to decay rate λ = log(2) / T (first-order exponential decay) and wraps each rate in a ConstantField{FT, 0}. Future plans may pass time-varying rates (e.g. temperature-dependent reaction rates) through the same field interface.
Common isotopes:
²²²Rn: half-life = 330_350.4 s (3.8235 days) → λ ≈ 2.098e-6 s⁻¹
⁸⁵Kr: half-life = 3.394e8 s (10.76 years) → λ ≈ 2.042e-9 s⁻¹
AtmosTransport.Operators.Chemistry.NoChemistry Type
NoChemistry()Identity operator — apply! is a no-op. Default for runs without active chemistry.
Step plans
The model step as data: the StepPlan that TransportModel.step! and DrivenSimulation execute and the cubed-sphere adjoint replays, its stages, and the stage executors. The step recorders (AbstractStepRecorder, NoStepRecorder) are in the top-level section above. See the forward model the adjoint transposes.
AtmosTransport.Operators.StepPlans Module
StepPlansThe model step as data: which operators run, in which order, with which time step, at every advection step and at the met-window boundaries.
A StepPlan is built once from the run configuration (physics cadence, the transport binary's window contract, the air-mass reset mode). Production executes it: TransportModel.step! runs its step stages and DrivenSimulation runs all four phases. The cubed-sphere adjoint executes the same plan with a tape recorder (AbstractStepRecorder) and walks the recorded tape backwards, so the forward model it differentiates is the production step by construction.
Phases, in the order a met window runs them:
| phase | runs |
|---|---|
window_advance | on entering a met window after the first (air-mass reset to the new window) |
window_start | before the first step of every met window |
step | every advection step |
window_end | after the last step of every met window |
AtmosTransport.Operators.StepPlans.AbstractPhysicsCadence Type
AbstractPhysicsCadenceWhen the physics blocks (convection, chemistry) run: every advection step (SubstepPhysics) or once per met window (WindowPhysics).
AtmosTransport.Operators.StepPlans.AbstractStageDt Type
AbstractStageDtThe time step a stage integrates over: StepDt (the advection step) or WindowDt (the met window).
AtmosTransport.Operators.StepPlans.AbstractStepStage Type
AbstractStepStageOne operator application of a StepPlan. Concrete stages: TransportStage, ConvectionStage, ChemistryBeforeStage, ChemistryAfterStage and AirMassResetStage. execute_stage!(stage, ops, timing, meteo, recorder) applies it.
AtmosTransport.Operators.StepPlans.AirMassResetStage Type
AirMassResetStage(mode, target)Replace the prognostic air mass by the met target (WindowStartAirMass() or WindowEndAirMass()), preserving the tracer mass (:preserve_tracer_mass) or the mixing ratio (:preserve_vmr).
AtmosTransport.Operators.StepPlans.ChemistryAfterStage Type
ChemistryAfterStage(StepDt() | WindowDt())The second half-step of a symmetrically split chemistry operator, or the full step of any other chemistry.
sourceAtmosTransport.Operators.StepPlans.ChemistryBeforeStage Type
ChemistryBeforeStage(StepDt() | WindowDt())The first half-step of a symmetrically split chemistry operator (constant-rate exponential decay); a no-op for other chemistry.
sourceAtmosTransport.Operators.StepPlans.ConvectionStage Type
ConvectionStage(StepDt() | WindowDt())The convection operator over the advection step or the met window.
sourceAtmosTransport.Operators.StepPlans.StepDt Type
StepDt() — the stage integrates over the advection step `dt`.AtmosTransport.Operators.StepPlans.StepOperators Type
StepOperators(; state, fluxes, grid, advection, advection_workspace,
diffusion = NoDiffusion(), diffusion_workspace = nothing,
emissions = NoSurfaceFlux(), convection = NoConvection(),
convection_workspace = nothing, convection_forcing = nothing,
chemistry = NoChemistry())The state, the step's face fluxes and the operators with their workspaces that the stages of a StepPlan act on. TransportModel provides them (step_operators(model)); the cubed-sphere adjoint builds them for its single-tracer replay.
AtmosTransport.Operators.StepPlans.StepPlan Type
StepPlan(; physics_cadence = nothing, window_contract = false,
air_mass_reset = :none)The model step of a run: the stages of its four phases (window_advance, window_start, step, window_end).
Without a window contract, or with
physics_cadence = :substep, every step runsChemistryBefore(dt) → Transport → Convection(dt) → ChemistryAfter(dt)(constant-rate decay split symmetrically around transport).With a transport binary's per-window contract (
window_contract = true) andphysics_cadence = :window(the default), every step runs the transport block only; the window starts with the first decay half over the window and ends with the air-mass reset to the window's end mass, convection over the window and the closing chemistry.air_mass_resetother than:noneresets the air mass to each newly entered window (window_advance) and, with a window contract, to the window's end mass before the window-end physics.
TransportModel.step! executes the step stages of the default plan; DrivenSimulation executes all phases of the plan of its configuration; the cubed-sphere adjoint records and transposes the same plan.
AtmosTransport.Operators.StepPlans.StepTiming Type
StepTiming(dt, window_dt = dt)The time steps of one advection step: its own dt and the length window_dt of the met window it belongs to.
AtmosTransport.Operators.StepPlans.SubstepPhysics Type
SubstepPhysics() — convection and chemistry run every advection step.AtmosTransport.Operators.StepPlans.TransportStage Type
TransportStage()The transport block of one advection step: the step-start halo refresh, the advection palindrome and its center (vertical diffusion and surface fluxes in the diffusion operator's coupling, or one diffusion solve V(dt) without a surface-flux operator).
AtmosTransport.Operators.StepPlans.WindowDt Type
WindowDt() — the stage integrates over the met window `window_dt`.AtmosTransport.Operators.StepPlans.WindowEndAirMass Type
WindowEndAirMass() — the reset target is the window's authoritative end air mass.AtmosTransport.Operators.StepPlans.WindowPhysics Type
WindowPhysics() — convection and chemistry run once per met window.AtmosTransport.Operators.StepPlans.WindowStartAirMass Type
WindowStartAirMass() — the reset target is the air mass of the met window just entered.AtmosTransport.Operators.StepPlans.execute_stage! Function
execute_stage!(stage, ops, timing, meteo, recorder)Apply one stage to ops.state.
AtmosTransport.Operators.StepPlans.execute_stages! Function
execute_stages!(stages::Tuple, ops, timing, meteo, recorder = NoStepRecorder())Apply stages in order. meteo is the run clock and met context (current_time(meteo), the air-mass reset targets); recorder observes every operator.
AtmosTransport.Operators.StepPlans.execute_step_stages! Function
execute_step_stages!(plan, ops, timing, meteo, recorder = NoStepRecorder())AtmosTransport.Operators.StepPlans.execute_window_advance! Function
execute_window_advance!(plan, ops, timing, meteo, recorder = NoStepRecorder())AtmosTransport.Operators.StepPlans.execute_window_end! Function
execute_window_end!(plan, ops, timing, meteo, recorder = NoStepRecorder())AtmosTransport.Operators.StepPlans.execute_window_start! Function
execute_window_start!(plan, ops, timing, meteo, recorder = NoStepRecorder())AtmosTransport.Operators.StepPlans.plan_stages Method
plan_stages(plan) -> TupleEvery stage of plan, in phase order (window_advance, window_start, step, window_end).