State API
For narrative coverage including the dry-basis contract and the tracer accessor surface, see State & basis.
AtmosTransport.State Module
StatePrognostic and diagnostic state containers for the basis-explicit transport architecture.
Provides:
CellState— cell-centered air mass + conservative tracer storageAbstractFaceFluxStatehierarchy — face mass fluxesAbstractStructuredFaceFluxState→StructuredFaceFluxState(am, bm, cm)AbstractUnstructuredFaceFluxState→FaceIndexedFluxState
MetState— upstream meteorological fields (consumed by flux builders, not transport)Tracer allocation and mixing-ratio utilities
AtmosTransport.State.AbstractFaceFluxState Type
AbstractFaceFluxStateRoot type for all face-centered mass flux representations.
The transport operator contract is written in terms of this abstract type. Concrete subtypes differ in storage layout to match the mesh's natural flux_topology, and carry a Basis <: AbstractMassBasis type parameter to enforce moist/dry safety.
AtmosTransport.State.AbstractMassBasis Type
AbstractMassBasisSupertype for mass-basis tags carried by cell and face-flux states.
sourceAtmosTransport.State.AbstractStructuredFaceFluxState Type
AbstractStructuredFaceFluxState <: AbstractFaceFluxStateFace fluxes stored as separate directional arrays on a logically rectangular mesh. Concrete subtypes expose am (x-face), bm (y-face), cm (z-face).
Structured cell-loop kernels access these arrays directly for performance.
sourceAtmosTransport.State.AbstractUnstructuredFaceFluxState Type
AbstractUnstructuredFaceFluxState <: AbstractFaceFluxStateFace fluxes stored as a single face-indexed array with explicit connectivity. Natural for reduced Gaussian and other unstructured meshes.
sourceAtmosTransport.State.CellState Method
CellState(m::AbstractArray; tracers...)Convenience constructor: CellState(m; CO2=rm_co2, SF6=rm_sf6). Defaults to DryBasis. The input 3D/2D arrays are COPIED into the packed tracers_raw buffer at construction — they are not aliased. Use get_tracer(state, :CO2) (or state.tracers.CO2) to read or mutate the stored tracer after construction.
AtmosTransport.State.CellState Method
CellState(::Type{B}, air_mass, tracers_raw, tracer_names)Direct construction from already-packed storage. Used by adaptation and low-level code that has a 4D buffer in hand.
sourceAtmosTransport.State.CubedSphereFaceFluxState Type
CubedSphereFaceFluxState{Basis}(am, bm, cm)Six-panel, halo-padded directional mass-flux storage for cubed-sphere transport. Each tuple contains one structured directional array per panel and uses the same mass basis as CubedSphereState.
sourceAtmosTransport.State.CubedSphereState Type
CubedSphereState(Basis, mesh, air_mass; tracers...)
CubedSphereState(Basis, air_mass; halo_width, tracers...)Panel-native prognostic state. Air mass is stored as six halo-padded three-dimensional panels. Tracers are packed into one trailing tracer axis on each panel, with names retained in storage order. Diagnostics and global sums exclude halos.
sourceAtmosTransport.State.FaceIndexedFluxState Type
FaceIndexedFluxState{Basis}(horizontal_flux, cm)Prepared substep mass transport for a connected-face mesh. Horizontal storage has shape (nfaces, Nz), while vertical storage has shape (ncells, Nz+1). Positive horizontal flux follows the mesh face orientation.
sourceAtmosTransport.State.MetState Type
MetState{PA <: AbstractArray, QA <: AbstractArray, M}Container for meteorological fields upstream of the transport core.
Fields
ps :: PA— surface pressure [Pa]. Layout:(Nx, Ny)or(ncells,).q :: QA— specific humidity [kg/kg]. Layout is the matching horizontal shape plus a trailing vertical dimension.metvars :: M— additional met-specific fields (winds, omega, diffusivities, etc.) as aNamedTuple. Content depends on the met driver.
Transport operators never receive MetState directly; preprocessing converts source meteorology into the current dry-basis transport-binary contract.
AtmosTransport.State.MoistBasis Type
MoistBasis <: AbstractMassBasisTag for total-air / moist mass.
sourceAtmosTransport.State.StructuredFaceFluxState Type
StructuredFaceFluxState{Basis}(am, bm, cm)Prepared substep mass transport on a logically rectangular grid. Arrays use shapes (Nx+1, Ny, Nz), (Nx, Ny+1, Nz), and (Nx, Ny, Nz+1). Positive horizontal fluxes point east/north; positive vertical flux points toward increasing level index. Basis must match the prognostic state.
sourceAtmosTransport.State.TracerAccessor Type
TracerAccessor{S}Lazy, allocation-free wrapper that forwards property-style tracer access (state.tracers.CO2) to get_tracer(state, :CO2). Returned by getproperty(::CellState, :tracers). Reading a tracer that does not exist throws KeyError.
state.tracers[:CO2] is also supported and forwards to get_tracer(state, :CO2) for code that prefers index syntax.
AtmosTransport.State.allocate_face_fluxes Method
allocate_face_fluxes(::FaceIndexedFluxTopology, nfaces, ncells, Nz;
FT=Float64, ArrayType=Array,
basis::Type{<:AbstractMassBasis}=DryBasis)Allocate zeroed face-indexed flux arrays for a connected-face mesh.
sourceAtmosTransport.State.allocate_face_fluxes Method
allocate_face_fluxes(::StructuredFluxTopology, Nx, Ny, Nz;
FT=Float64, ArrayType=Array,
basis::Type{<:AbstractMassBasis}=DryBasis)Allocate zeroed face flux arrays for a structured mesh. The returned StructuredFaceFluxState is tagged with the specified basis.
AtmosTransport.State.allocate_face_fluxes Method
allocate_face_fluxes(mesh::AbstractHorizontalMesh, Nz; kwargs...)Allocate a flux container using the mesh's natural face-connected topology.
sourceAtmosTransport.State.allocate_face_fluxes Method
allocate_face_fluxes(mesh::AbstractStructuredMesh, Nz; kwargs...)Allocate a flux container using the mesh's natural structured topology.
sourceAtmosTransport.State.allocate_tracers Method
allocate_tracers(names::NTuple{N, Symbol}, Nx, Ny, Nz;
FT=Float64, ArrayType=Array, fill_value=zero(FT))Allocate a NamedTuple of 3D model tracer-storage arrays. Still useful for test fixtures that want to pre-build per-tracer 3D arrays for a CellState(m; tracers...) keyword-form constructor call.
AtmosTransport.State.eachtracer Method
eachtracer(state::CellState)Iterate name => tracer_slice pairs for every tracer in state, in storage order. The yielded shape matches the previous pairs(::NamedTuple) contract so callers that destructure for (name, rm) in eachtracer(state) continue to work.
AtmosTransport.State.flux_basis Method
flux_basis(state) → AbstractMassBasisReturn the mass flux basis tag for the given flux state.
sourceAtmosTransport.State.get_tracer Method
get_tracer(state::CellState, name::Symbol)
get_tracer(state::CellState, idx::Integer)Return a view of the model tracer-storage slice. For a structured grid with state.tracers_raw :: Array{FT, 4}, this is selectdim(state.tracers_raw, 4, idx), a contiguous SubArray{FT, 3} (because Julia is column-major and the tracer axis is the slowest-varying). Mutations through the returned view are reflected in state.tracers_raw.
Throws KeyError(name) if name is not a tracer in state.
AtmosTransport.State.mixing_ratio Method
mixing_ratio(state::CellState, name::Symbol)Compute mixing ratio q = tracer_storage / air_mass for the named tracer.
AtmosTransport.State.ntracers Method
ntracers(state::CellState) -> IntNumber of tracers carried by state.
AtmosTransport.State.set_uniform_mixing_ratio! Method
set_uniform_mixing_ratio!(state::CellState, name::Symbol, χ)Set tracer name to uniform mixing ratio χ: tracer_mass = χ × air_mass.
AtmosTransport.State.total_air_mass Method
total_air_mass(state::CellState) -> scalarSum of air mass across all cells and levels.
sourceAtmosTransport.State.total_mass Method
total_mass(state::CellState, name::Symbol) -> scalarSum the named tracer's conservative model storage across all cells and levels. For dry VMR tracers this is sum(χ_dry × dry_air_mass), not physical kg species unless an explicit molecular-weight conversion is applied.
AtmosTransport.State.tracer_index Method
tracer_index(state::CellState, name::Symbol) -> Union{Int, Nothing}Index of the tracer named name in state.tracer_names, or nothing if absent.
AtmosTransport.State.tracer_name Method
tracer_name(state::CellState, idx::Integer) -> SymbolName of the tracer at index idx. Throws BoundsError if out of range.
AtmosTransport.State.tracer_names Method
tracer_names(state::CellState) -> NTuple{Nt, Symbol}Names of all tracers in state, in stored order.
Fields
Time-varying and panel-wise field types used by State (Kz caches, profile fields, PBL parameters, …).
AtmosTransport.State.Fields Module
FieldsTime-varying field abstraction.
AbstractTimeVaryingField{FT, N} is the common interface presented to physics operators for rate-like and field-like inputs (chemistry decay rates, Kz diffusion fields, ...). Concrete types back different sources — constant, stepwise from file, derived from meteorology — with the same kernel code downstream.
Required interface (every concrete type):
field_value(f, idx::NTuple{N, Int}) -> FT— kernel-safe readupdate_field!(f, t::Real) -> f— refresh caches for timet
Concrete types:
ConstantField{FT, N}— one scalar presented as a rank-N field.ProfileKzField{FT}— rank-2/3 vertical profile, horizontally uniform.PreComputedKzField{FT, A}— rank-2/3 wrapper over a spatial array.DerivedKzField{FT, ...}— Beljaars-Viterbo Kz from surface fields.WindowPBLKzField{FT, ...}— cubed-sphere per-window Beljaars-Viterbo Kz.LocalHoltslagBovilleKzField{FT, ...}— cubed-sphere GEOS VDIFF local-Kz cache.StepwiseField{FT, N, A, B, W}— piecewise-constant in time at any rank.
PBLPhysicsParameters{FT} carries the physical constants for DerivedKzField.
AtmosTransport.State.Fields.AbstractCubedSphereField Type
AbstractCubedSphereField{FT}Panel-native field contract for cubed-sphere operators.
Unlike AbstractTimeVaryingField, which describes a single rank-N spatial field, cubed-sphere runtime paths keep the panel boundary explicit. A concrete cubed-sphere field therefore owns one structured rank-3 field per panel and exposes them through panel_field.
AtmosTransport.State.Fields.AbstractTimeVaryingField Type
AbstractTimeVaryingField{FT, N}Common interface for rate-like / field-like inputs to physics operators. FT <: AbstractFloat is the element type; N :: Int is the spatial rank (0 = scalar, 2 = surface, 3 = volume).
See field_value and update_field! for the required methods.
AtmosTransport.State.Fields.ConstantField Type
ConstantField{FT, N}(value)A scalar value presented as an AbstractTimeVaryingField{FT, N}. field_value ignores its index and returns value. update_field! is a no-op. Storage is one scalar; backend-agnostic by construction.
Examples
rate = ConstantField{Float64, 0}(2.098e-6) # chemistry decay rate
kz_test = ConstantField{Float32, 3}(1.0f0) # idealized diffusion KzAtmosTransport.State.Fields.CubedSphereField Type
CubedSphereField{FT, F}(panels::NTuple{6, F})Panel-native cubed-sphere field wrapper.
Each panel field must satisfy the standard structured field contract:
panel_field(f, p) isa AbstractTimeVaryingField{FT, 3}field_value(panel_field(f, p), (i, j, k)) -> FTupdate_field!(panel_field(f, p), t)
This keeps the cubed-sphere operator boundary honest without forcing a fake global 4D/5D field abstraction onto code that already runs panel by panel.
sourceAtmosTransport.State.Fields.DerivedKzField Type
DerivedKzField(; surface, delp, cache, params = PBLPhysicsParameters{FT}())A rank-3 AbstractTimeVaryingField{FT, 3} whose values are Kz [m²/s] computed per column from Beljaars-Viterbo surface-field closure.
Inputs
| Name | Type | Role |
|---|---|---|
surface | NamedTuple of four rank-2 TimeVaryingFields | (pblh, ustar, hflux, t2m) fields |
delp | rank-3 TimeVaryingField | Layer pressure thickness [Pa], k=1 at TOA |
cache | AbstractArray{FT, 3} | Backing storage for computed Kz, size (Nx, Ny, Nz) |
params | PBLPhysicsParameters{FT} | Physical constants |
Interface
field_value(f, (i, j, k))→f.cache[i, j, k]. Kernel-safe.update_field!(f, t)→Refreshes every input field at time
t(surface + delp).Recomputes every cell's Kz on the cache backend.
Kz is stored at cell centers. Consumers that need interface Kz (e.g. a Thomas solve) should interpolate between adjacent k-levels.
Vertical convention
k = 1 is TOA; k = Nz is surface. This matches the legacy _pbl_diffuse_kernel! which accumulates p_top upward from 0 Pa and integrates hydrostatic dz downward. Configs that provide ground-up delp must flip the k axis before passing in.
Example (test-style construction)
surface = (
pblh = ConstantField{Float64, 2}(1000.0),
ustar = ConstantField{Float64, 2}(0.3),
hflux = ConstantField{Float64, 2}(100.0),
t2m = ConstantField{Float64, 2}(295.0),
)
delp = PreComputedKzField(fill(3000.0, 4, 3, 10)) # 10 layers of 3 kPa
cache = zeros(Float64, 4, 3, 10)
f = DerivedKzField(; surface, delp, cache)
update_field!(f, 0.0)
Kz_surface_cell = field_value(f, (1, 1, 10)) # populated m²/sAtmosTransport.State.Fields.LocalHoltslagBovilleKzField Type
LocalHoltslagBovilleKzField(host_cache; params = PBLPhysicsParameters{FT}())Panel-native Kz cache for the GEOS/GCHP VDIFF runtime path.
The cache is refreshed from the active window's GEOS VDIFF payload (vdiff_u, vdiff_v, vdiff_t, vdiff_qv) plus PBL surface fields. This is a local Holtslag-Boville Kz closure: it derives column geometry + free-tropospheric shear enhancement from GEOS T/qv/wind profiles, with the existing Beljaars-Viterbo surface-layer shape inside the diagnosed PBL.
⚠ NOT full GCHP VDIFF parity
Two intentional divergences from GCHP's vdiff_mod.F90:
No non-local counter-gradient term. GCHP's
pbldif(vdiff_mod.F90:1237-1254) computes counter-gradient termscgs = fak3/(pblh·wm),cgh = khfs·cgsand injects them into the θ and qv profiles before the implicit solve. We apply only the LOCAL Kz, so daytime PBL lofting of surface-emitted tracers is systematically weaker than GCHP — typically 10–30 % less mass above the PBL top for surface sources. Usekind = "tm5_beljaars_viterbo_local_kz"if you want to be explicit that no parameterization claims GCHP parity.Surface-flux coupling default mismatch. GCHP applies emissions as a boundary condition inside one combined turbulence step (
vdiff_mod.F90:679,gchp_chunk_mod.F90:1296). Our defaultSplitSurfaceFluxCouplingdoesV(dt/2) → S(dt) → V(dt/2)Strang instead. For GCHP parity in long integrations, set[surface_flux].coupling = "boundary"(which selectsDiffusiveSurfaceFluxBoundary). The recipe validator emits a warning at config-load when this Kz field is selected without the boundary-coupling switch.
See memory/diffusion_full_pipeline_audit_2026_05_25.md for the full audit chain and pending mass-flux conservation fix.
Why no non-local kernel here
In GCHP's full VDIFF the non-local term enters as an additive RHS source in the implicit tridiagonal solve, NOT a modification of Kz itself:
∂(m·q)/∂t = ∂/∂z [ρ·Kz·(∂q/∂z - γ_q)]where γ_q = a·w*·q*/wm²/pblh is a scaled convective-velocity expression of the surface flux. A real non-local kernel would need:
Convective-velocity diagnostics (
w*,wm, etc.) added to the per-window refresh — currently onlypblh, ustar, hflux, t2mare in the surface payload.A per-tracer
γ_qderivation. For our offline pipeline this is ill-defined because surface emissions are applied byapply_surface_flux!as a SEPARATE operator outside the diffusion solve, so the surface-source pattern that the GCHP counter-gradient exists to handle doesn't appear inside our diffusion step.A new RHS-source term in
_vertical_diffusion_cs_*kernels + a matching adjoint kernel.
This is deferred indefinitely; the field is correctly named Local… so users opting into GCHP-style VDIFF know what they get.
AtmosTransport.State.Fields.PBLPhysicsParameters Type
PBLPhysicsParameters{FT}(; kwargs...)Physical constants for the Beljaars-Viterbo (revised LTG) boundary- layer diffusivity parameterization, as implemented in TM5 (diffusion.F90, bldiff). Defaults reproduce the values used in the legacy PBLDiffusion.
| Field | Default | Units | Meaning |
|---|---|---|---|
β_h | 15.0 | — | Businger-Dyer stability parameter |
Kz_bg | 0.1 | m²/s | Free-tropospheric background Kz |
Kz_min | 0.01 | m²/s | Safety floor inside the PBL |
Kz_max | 500.0 | m²/s | Safety ceiling inside the PBL |
kappa_vk | 0.41 | — | von Kármán constant |
gravity | 9.80665 | m/s² | Gravitational acceleration |
cp_dry | 1004.64 | J/(kg·K) | Dry-air specific heat at const. p |
rho_ref | 1.225 | kg/m³ | Reference density for H_kin |
R_dry is derived from cp_dry as cp_dry / 3.5 (ideal diatomic, cp = 7/2 R_dry), matching the legacy convention.
AtmosTransport.State.Fields.PreComputedKzField Type
PreComputedKzField{FT, N, A}(data::AbstractArray{FT, N})A rank-2/3 AbstractTimeVaryingField{FT, N} backed directly by a spatial array. field_value(f, idx) returns f.data[idx...] — the no-abstraction path for Kz profiles read from a pre-computed source (met binary snapshot, GCHP-style 3D diffusivity export, etc.).
Time variation, if any, is handled by the caller: mutate f.data in place when the current met window advances, then call (or skip) update_field! — this method is a no-op and does not own the storage. A more structured stepwise-in-time backing (e.g. a 4D buffer with an internal window index) can be added later as a separate concrete type; keeping this one minimal makes the contract transparent.
Backend
A <: AbstractArray{FT, N} is parametric: Array{FT, N} for CPU, CuArray{FT, N} / MtlArray{FT, N} for GPU runs. field_value indexes the array element-wise, which is kernel-safe on all three backends.
Examples
# CPU: static 3D Kz snapshot
Kz = fill(1.0, 144, 72, 34)
f = PreComputedKzField(Kz)
field_value(f, (1, 1, 1)) # 1.0
field_value(f, (144, 72, 34)) # 1.0
# Updating the snapshot between met windows (caller-owned)
Kz .= 2.0
field_value(f, (1, 1, 1)) # 2.0 — same f, updated dataAtmosTransport.State.Fields.PrecomputedCSDkgField Type
PrecomputedCSDkgField(host_cache)Cubed-sphere interface air-mass exchange field loaded from a binary :dkg payload. dkg[i,j,k] is the TM5 vertical-diffusion exchange rate [kg s⁻¹] between top-down layers k and k+1; the final level is the zero-flux surface boundary.
Unlike a Kz field, this payload already includes the preprocessing-time layer mass and virtual-temperature geometry. The runtime must therefore consume it directly rather than reconstructing an interface coefficient from layer-centre Kz and a second, potentially inconsistent, dz profile.
AtmosTransport.State.Fields.ProfileKzField Type
ProfileKzField{FT, V, N}(profile::AbstractVector{FT})A rank-2/3 AbstractTimeVaryingField{FT, N} backed by a vertical vector of length Nz. field_value(f, idx) returns f.profile[idx[end]] — horizontally uniform, vertically varying. Time- independent (constant profile); update_field! is a no-op.
Useful for idealized test cases: Chapter-style exponential Kz profiles (K0 * exp(-z/H)), canonical PBL-capped profiles, or any column diagnostic where the Kz shape is known analytically and horizontal variation is irrelevant. Not the operational path — operational Kz comes from DerivedKzField (Beljaars-Viterbo) or PreComputedKzField (from binary).
Storage
Parametric on the vector type V <: AbstractVector{FT}. The default constructor accepts a host Vector{FT}; Adapt.adapt can then convert the field to device storage (e.g., CuArray{FT, 1}) for GPU kernel launches. field_value dispatches through the vector's getindex, which is kernel-safe on all supported backends once the vector lives on-device.
Examples
# CPU — single-scale exponential Kz
Kz_profile = 1.0 .* exp.(-(0:33) ./ 5.0)
f = ProfileKzField(collect(Kz_profile))
field_value(f, (1, 1, 1)) # 1.0
field_value(f, (144, 72, 1)) # 1.0 — same k, different (i,j)
field_value(f, (1, 1, 10)) # ≈ 0.135
# GPU — adapt to a CuArray backing before kernel launch
# using CUDA, Adapt
# f_gpu = Adapt.adapt(CuArray, f) # f_gpu.profile isa CuArray{Float64, 1}AtmosTransport.State.Fields.StepwiseField Type
StepwiseField{FT, N, A, B, W}(samples, boundaries, current_window)A rank-N AbstractTimeVaryingField{FT, N} that is piecewise-constant in time. Stores a sequence of N_win spatial snapshots plus N_win + 1 window boundaries. field_value reads from the snapshot for the window containing the current time; update_field!(f, t) binary- searches the boundaries and caches the index of the current window.
Designed for CATRINE-style emission inventories that arrive as window- averaged values (monthly, annual, weekly). The sample for window n is the average value over [boundaries[n], boundaries[n+1]).
Layout
samples :: AbstractArray{FT, N + 1} # rank N + 1
# last dim iterates over windows
# size(samples)[1:N] = spatial shape
# size(samples, N+1) = N_win
boundaries :: AbstractVector{<:Real} # length N_win + 1, sorted
# boundaries[n], boundaries[n+1]
# bound window n
current_window :: AbstractVector{Int} # length 1, holds the index that
# `field_value` readsThe spatial prefix of samples matches the field's rank: rank-2 field → samples::AbstractArray{FT, 3} with shape (Nx, Ny, N_win). Rank-3 field → samples::AbstractArray{FT, 4} with (Nx, Ny, Nz, N_win).
Why current_window is a 1-element array
Base.RefValue{Int} would be the idiomatic mutable Int cache, but it is not kernel-safe on GPU — a Ref dereference inside a KA kernel pulls from host memory. Storing the index in a 1-element array lets Adapt.adapt convert it to device storage alongside samples, so the kernel reads current_window[1] as a device-local memory access. Overhead: one extra device load per field_value call, broadcast across all kernel threads (same value for every thread), effectively free.
Kernel-safety
After update_field!(f, t) has been called by the operator, the kernel can call field_value(f, idx) freely. The call is allocation-free and type-stable. The operator is responsible for calling update_field! on the host before launching the kernel.
Out-of-bounds time
update_field!(f, t) with t < boundaries[1] or t >= boundaries[end] throws an ArgumentError. This is stricter than clamping but avoids silently extrapolating the first/last window — which would bias emission totals. Callers that need cyclic coverage can wrap in a CyclicField (future plan) or extend boundaries explicitly.
Examples
# Rank-2 (surface) StepwiseField: monthly CO2 emission inventory
# over 12 months, Nx × Ny = 144 × 72.
samples = rand(144, 72, 12) # kg/s per cell
boundaries = [FT(m * 30 * 86400) for m in 0:12] # month starts, seconds
f = StepwiseField(samples, boundaries)
update_field!(f, 15.0 * 86400) # day 15 → window 1
field_value(f, (10, 20)) # = samples[10, 20, 1]
update_field!(f, 65.0 * 86400) # day 65 → window 3
field_value(f, (10, 20)) # = samples[10, 20, 3]Adapt / GPU
using CUDA, Adapt
f_gpu = Adapt.adapt(CuArray, f)
# f_gpu.samples isa CuArray{FT, 3}
# f_gpu.current_window isa CuArray{Int, 1}
# field_value(f_gpu, (i, j)) now kernel-safe on GPUAtmosTransport.State.Fields.StepwiseField Method
StepwiseField(samples, boundaries, current_window)Three-argument form that preserves an explicit current_window buffer. Primarily used by Adapt.adapt_structure to carry the cached window index across a host → device conversion without losing state.
AtmosTransport.State.Fields.StepwiseField Method
StepwiseField(samples, boundaries)Outer convenience constructor. Infers FT from eltype(samples) and N from ndims(samples) - 1. Initializes current_window = [1]; the first call to update_field!(f, t) sets the real index.
AtmosTransport.State.Fields.WindowPBLKzField Type
WindowPBLKzField(host_cache; params = PBLPhysicsParameters{FT}())Panel-native Kz cache for cubed-sphere window-driven PBL diffusion.
host_cache is an NTuple{6, Array{FT,3}} with interior panel shape (Nc, Nc, Nz). The runtime refreshes it from the active transport window's raw PBL surface fields and dry air mass whenever the met window advances. The diffusion kernels read the wrapped PreComputedKzFields.
Cadence — window-constant by design
update_field!(::WindowPBLKzField, ::Real) is a deliberate no-op. The surface fields that feed the Kz closure (pblh, ustar, pbl_hflux, t2m) are loaded once per met window (hourly archive); the diagnosed Kz inherits that cadence. Refreshing per substep without first interpolating the surface fields would produce identical output — so the no-op accurately reflects the data flow.
This matches the surface-state smoothness on the typical 1 h archive cadence (pblh and friends evolve on a 10 min - 1 h characteristic scale). The associated systematic error in tracer diffusion is well below the operator-level mass-conservation tolerance (10⁻⁷). TM5 and GCHP refresh Kz at every dynamic step against state that's also updated each dynamic step; matching that cadence on offline transport would require sub-hourly surface forcing and per-substep linear interpolation, neither of which is wired up here. Tracked in memory/diffusion_full_pipeline_audit_2026_05_25.md.
AtmosTransport.State.Fields.field_value Function
field_value(f::AbstractTimeVaryingField{FT, N}, idx::NTuple{N, Int}) -> FTReturn the field's current value at spatial index idx.
Contract (kernel-safe): allocation-free, type-stable, pure with respect to f. Called from inside KernelAbstractions kernels. For N = 0, idx is the empty tuple ().
Every concrete subtype of AbstractTimeVaryingField must implement a method of field_value.
AtmosTransport.State.Fields.integral_between Method
integral_between(f::StepwiseField, t1::Real, t2::Real, idx::NTuple{N, Int}) -> FTExact integral of the piecewise-constant field between times t1 and t2 at spatial index idx. Sums samples[idx..., n] × overlap(n) over every window n that intersects [t1, t2], where overlap(n) = max(0, min(t2, b[n+1]) - max(t1, b[n])).
Host-side helper — not kernel-safe. Included for operators that care about the time-integrated flux (e.g. an emissions operator that integrates across a window boundary within a single step). The surface flux kernel uses the instantaneous field_value × dt form and does NOT consume integral_between.
AtmosTransport.State.Fields.panel_field Function
panel_field(f, p) -> AbstractTimeVaryingFieldReturn the per-panel field stored at panel index p (1..6) inside an AbstractCubedSphereField. Subtypes implement this to expose their panel storage.
AtmosTransport.State.Fields.refresh_pbl_kz_cache! Method
refresh_pbl_kz_cache!(field, surface, air_mass, cell_areas; halo_width)Recompute field from a window's raw PBL surface forcing and dry air mass. air_mass may be halo-padded; halo_width selects the interior. The computed host cache is copied back into the field panels, which may be CPU or device arrays.
AtmosTransport.State.Fields.update_field! Function
update_field!(f::AbstractTimeVaryingField, t::Real) -> fRefresh any caches so that subsequent field_value calls return the field's value at simulation time t. Runs on the host (not kernel- safe itself); may be expensive for derived fields. Called once per apply! by an operator, before any kernel launch that reads the field.
For time-independent fields (e.g. ConstantField), this is a no-op. Returns f for chaining.
AtmosTransport.State.Fields.update_field! Method
update_field!(f::StepwiseField, t::Real) -> fBinary-search f.boundaries for the window n such that f.boundaries[n] <= t < f.boundaries[n+1] and cache n into f.current_window[1]. Host-side; not kernel-safe itself.
Throws ArgumentError if t is outside [f.boundaries[1], f.boundaries[end]).