Tracers

ClimaAtmos provides automatic treatment of conserved scalar tracers at two levels: grid-scale (resolved) and sub-grid scale (SGS, inside prognostic EDMF updrafts). Both levels use an auto-discovery mechanism: any field that follows the naming convention is automatically picked up for transport, diffusion, and other generic operations — no additional code changes are required.

Grid-Scale Tracers

Grid-scale tracers are density-weighted scalars $\rho \chi$ stored at cell centers in the prognostic state Y.c.

Naming convention

A grid-scale tracer is identified by a name that starts with ρ followed by the scalar name, e.g. ρq_tot, ρq_lcl, ρn_rai. The utility function gs_tracer_names(Y) discovers all such tracers automatically by inspecting Y.c and excluding non-tracer fields (ρ, ρe_tot, uₕ, ρtke, sgsʲs).

Automatically handled operations

OperationDescription
Horizontal advectionFlux-form divergence of $\rho \chi \boldsymbol{u}_h$
Vertical advectionUpwinded vertical transport
Vertical diffusionEddy-diffusivity-based mixing
Hyperdiffusion4th-order $\nabla^4$ stabilization with DSS

The iteration utility foreach_gs_tracer(f, Y...) applies a function f to each discovered tracer.

SGS Tracers (Prognostic EDMF)

When prognostic EDMF is enabled, each updraft carries its own set of scalar fields inside Y.c.sgsʲs.:(j). The utility function sgs_tracer_names(Y) discovers all scalars in the first updraft (Y.c.sgsʲs.:(1)) and excludes the core EDMF variables ρa, mse, and q_tot, which receive physics-specific treatment.

Naming convention

An SGS tracer χ in Y.c.sgsʲs.:(j) maps to a grid-scale density-weighted counterpart ρχ in Y.c. For example:

SGS field (in sgsʲs.:(j))Grid-scale field (in Y.c)
q_lclρq_lcl
q_raiρq_rai
n_raiρn_rai
A (user-defined)ρA

This pairing is enforced by get_ρχ_name(χ_name) which constructs ρχ from χ.

Automatically handled operations

The following operations are auto-discovered for all SGS tracers. No code changes are needed when adding a new tracer:

OperationFilePattern
Horizontal advectionadvection.jlfor χ_name in sgs_tracer_names(Y)
Vertical advection (advective form)advection.jlfor χ_name in sgs_tracer_names(Y)
Entrainment/detrainment mixingedmfx_entr_detr.jlfor χ_name in sgs_tracer_names(Y)
SGS mass flux (draft + environment → grid mean)edmfx_sgs_flux.jlfor χ_name in sgs_tracer_names(Y)
SGS diffusive flux (grid mean)edmfx_sgs_flux.jlfor χ_name in sgs_tracer_names(Y)
Updraft vertical diffusionmass_flux_closures.jlfor χ_name in sgs_tracer_names(Y)
Updraft constraint enforcementmass_flux_closures.jlfor χ_name in sgs_tracer_names(Y)
Rayleigh sponge dampingremaining_tendency.jlfor χ_name in sgs_tracer_names(Y)

All SGS tracers (cloud species and precipitation alike) receive the same reduced vertical diffusion coefficient (α_vert_diff_microphysics).

Adding a New Passive Tracer

To add a new passive tracer A that is transported through the full grid-scale + SGS system, the only changes needed are:

Step 1: Add ρA to the grid-scale prognostic state

In prognostic_variables.jl, add ρA to the center variables:

ρA = ρ * physical_state.A

This gives automatic grid-scale advection, diffusion, hyperdiffusion, and surface flux — all handled by foreach_gs_tracer.

Step 2: Add A to the SGS updraft state

In prognostic_variables.jl, add A to the SGS struct:

sgsʲs = uniform_subdomains((; ρa, mse, q_tot, A = physical_state.A), turbconv_model)

This gives automatic SGS entrainment, mass flux, diffusive flux, vertical diffusion, updraft constraints, advection, and sponge damping — all handled by sgs_tracer_names.

Step 3: Initial condition

Set the initial value of A in the setup file (e.g. Bomex.jl):

A = FT(1.0)  # constant initial concentration

That's it — no tendency code changes needed.

Step 4 (if using implicit solver): Update the Jacobian

The implicit solver's Jacobian (manual_sparse_jacobian.jl) uses hardcoded tracer lists for performance reasons (unrolled_foreach with compile-time tuples). Adding a new tracer to the Jacobian requires manually editing several locations in jacobian_cache (sparsity pattern) and update_jacobian! (numeric updates). Search for existing microphysics tracer names (e.g. q_lcl, q_rai) to find each block and add the new tracer alongside them.

Key locations to update:

LocationWhat to add
condensate_names / condensate_mass_names in jacobian_cache@name(c.ρA)
sgs_condensate_names / sgs_condensate_mass_names in jacobian_cache@name(c.sgsʲs.:(1).A)
SGS vertical diffusion blockAppend to sgs_microphysics_tracers tuple
SGS entrainment blockAppend to sgs_microphysics_tracers tuple
Grid-mean + SGS mass flux blockAppend to microphysics_tracers tuple

For moisture species that affect pressure, buoyancy, or have a sedimentation velocity, additional blocks need updating (pressure gradient, sedimentation, SGS pressure/buoyancy). Search for existing species like q_rai to locate each block.

Note

A passive tracer that doesn't affect thermodynamics and has no sedimentation can often run without Jacobian entries. The implicit solver will still converge, just more slowly for that variable.

Operations that remain manual

OperationReason
Initial / boundary conditionsProblem-specific
Source / sink termsPhysics-specific
Jacobian blocks (implicit solver)See Step 4 above
Diagnostics outputUser must define short names

Implementation details

The auto-discovery relies on two key patterns:

  1. Field-name predicates_is_sgs_tracer_name and is_ρ_weighted_name filter the top-level field names at the type level, enabling unrolled_filter to resolve the tracer list with zero runtime cost.

  2. MatrixFields.get_field + FieldName — tracer fields are accessed via MatrixFields.get_field(Y.c.sgsʲs.:(1), χ_name) using the discovered FieldName. This is equivalent to direct property access (e.g. Y.c.sgsʲs.:(1).q_lcl) and compiles to the same code.