Surface Conditions
The lower boundary is where the atmosphere exchanges momentum, heat, and moisture with whatever lies beneath it (ocean, land, sea ice, or an idealized slab). ClimaAtmos collects everything controlling this boundary into one object, AtmosSurface, stored as atmos.surface. It is read at each step to fill p.precomputed.sfc_conditions, the surface fluxes and values consumed as boundary conditions by the dynamical core, radiation, and turbulence schemes.
The User Guide covers the options and how to choose; the Developer Guide covers the design, data flow, and how to extend or debug it.
User Guide
The four knobs
AtmosSurface has four fields, each with one purpose:
flux_scheme: computes turbulent fluxes from air–surface differences in temperature, humidity etc.temperature: sets the surface temperatureT_sfc.boundary_overrides: pins surface properties at user-specified values.surface_albedo: sets the shortwave reflectivity seen by radiation (distinct direct and diffuse components).
Set these directly when building a model, or let them be chosen by a setup or by YAML keys.
Flux scheme (flux_scheme)
The closure turning the surface–to–lowest-level difference into turbulent fluxes of momentum, heat, and moisture:
MoninObukhov: Monin–Obukhov Similarity Theory (MOST); fluxes follow from roughness length and near-surface stability. Heat fluxes (shf/lhforθ_flux/q_flux) orustarmay instead be prescribed (common for LES). For time-varying prescribed fluxes, passfluxesas a callable(t, FT) -> HeatFluxes/θAndQFluxes— it is resolved once per update (e.g. TRMM_LBA's diurnal SHF/LHF), whilez0/ustarstay constant.ExchangeCoefficients: bulk fluxes with fixedCd/Ch; simpler and cheaper, for idealized constant exchange coefficients (rather than coefficients determined by MOST).nothing: no atmos-side computation; an external driver supplies the conditions (see Coupling).
Temperature source (temperature)
What T_sfc is; the flux scheme then uses it (and surface humidity) for the air–surface gradients:
AnalyticTemperature:T_sfc = f(coordinates, params, t), per point. Covers a uniform constant (AnalyticTemperature(Returns(FT(300)))), a zonally-symmetric SST, or a time-varying profile (e.g., GABLS).SlabOceanTemperature: prognostic;T_sfcread fromY.sfc.T, evolved by a slab-ocean energy budget. The only type that adds a prognostic state.ExternalTemperature: read from a time-varying external input; valid only when the setup populatesexternal_forcing.surface_inputs.CoupledTemperature: read from aFieldthe coupler writes into (see Coupling).
There is no dedicated constant type. Use AnalyticTemperature(Returns(FT(300))), wrapping the value in FT(...) to keep the broadcast type-stable.
Boundary overrides (boundary_overrides)
By default, surface values come from physics (pressure hydrostatically extrapolated, humidity saturated at T_sfc, zero winds, unit gustiness/moisture availability). SurfaceBoundaryOverrides pins any of these to a fixed value; each field defaults to nothing (use the physical default). Most idealized LES setups override p and q_vap.
Albedo (surface_albedo)
Sets the shortwave reflectivity passed to the radiation scheme. Three models:
ConstantAlbedo: a single value applied to both direct and diffuse shortwave.RegressionFunctionAlbedo: the Jin et al. (2011) ocean parameterization — a solar-zenith-angle-dependent direct albedo plus a separate diffuse albedo, with wind-speed-dependent surface roughness.CouplerAlbedo: albedo supplied by an external driver (the coupler).
Direct vs. diffuse The model carries distinct direct_sw_surface_albedo and diffuse_sw_surface_albedo fields. ConstantAlbedo sets them equal, RegressionFunctionAlbedo computes them separately.
Spectral All three models write a single value across every shortwave band, and the RegressionFunctionAlbedo scheme treats the refractive index as wavelength-independent. The RRTMGP interface arrays are band-resolved ((nbnd_sw, ncol)), so per-band albedo is a supported extension point but it would require a model that fills bands with distinct values.
Longwave surface reflectivity Albedo is shortwave-only, longwave surface reflectivity is handled separately through surface_emissivity.
See the Ocean Surface Albedo page for the Jin (2011) RegressionFunctionAlbedo formulation..
Choosing
flux_scheme and temperature are independent axes, and you set both (the other two fields take defaults). Each row below is a compatible pair, not an either/or:
| If you want… | flux_scheme | temperature |
|---|---|---|
| Stability-dependent fluxes over a prescribed SST | MoninObukhov(; z0 = …) | AnalyticTemperature(…) |
| Fixed-coefficient bulk fluxes | ExchangeCoefficients(; Cd, Ch) | AnalyticTemperature(…) |
| Prescribed heat fluxes (constant or time-varying) | MoninObukhov(; z0, shf, lhf) or MoninObukhov(; z0, fluxes = (t,FT)->…) | AnalyticTemperature(…) |
| An interactive slab ocean surface | MoninObukhov(…) | SlabOceanTemperature(…) |
| Surface temperature from data | MoninObukhov(…) | ExternalTemperature(…) |
| Coupler owns the surface (atmos skips fluxes) | nothing | unused — coupler writes sfc_conditions |
| Coupler sets SST; atmos computes fluxes | MoninObukhov(…) | CoupledTemperature(field) |
When you set shf/lhf (or θ_flux/q_flux), those fluxes are used as prescribed: MOST does not compute them. They appear under MoninObukhov only because the prescribed-flux path currently lives inside that type (a historical conflation; see the Developer Guide). The required z0 is used solely for the momentum closure, and only when ustar is not also prescribed — when both fluxes and ustar are given (as in every idealized LES setup), MOST does nothing and the surface is fully prescribed.
Setting the surface in a runscript
Build an AtmosSurface and hand it to AtmosModel. For example, Monin–Obukhov fluxes over a fixed 290 K sea surface with a constant albedo:
import ClimaAtmos as CA
import ClimaAtmos.SurfaceConditions as SC
FT = Float64
surface = CA.AtmosSurface(;
flux_scheme = SC.MoninObukhov(; z0 = FT(1e-4)),
temperature = SC.AnalyticTemperature(Returns(FT(290))),
surface_albedo = CA.ConstantAlbedo{FT}(; α = FT(0.07)),
# boundary_overrides defaults to all-`nothing` (physical defaults)
)
model = CA.AtmosModel(; surface, microphysics_model = CA.DryModel())Omitted fields take their defaults. You can also pass the surface fields directly to AtmosModel (CA.AtmosModel(; flux_scheme = …, temperature = …)), which assembles the AtmosSurface for you. To swap in an interactive slab ocean use temperature = SC.SlabOceanTemperature{FT}(); for prescribed heat fluxes, flux_scheme = SC.MoninObukhov(; z0 = FT(1e-4), shf = …, lhf = …).
Configuring from YAML
Three of the four AtmosSurface fields are YAML-configurable (resolved by AtmosSurface(::AtmosConfig, params, FT; setup_type)); setup-provided pieces take precedence over these defaults:
surface_setupsetsflux_scheme:"DefaultExchangeCoefficients"(default),"DefaultMoninObukhov", or"PrescribedSurface"(→nothing).prognostic_surfacesetstemperature:"PrescribedSST"(default) or"SlabOceanSST"(→SlabOceanTemperature).albedo_modelsetssurface_albedo:"ConstantAlbedo"(default),"RegressionFunctionAlbedo", or"CouplerAlbedo".
For example:
surface_setup: "DefaultMoninObukhov" # flux_scheme
prognostic_surface: "PrescribedSST" # temperature
albedo_model: "ConstantAlbedo" # surface_albedoThe fourth field, boundary_overrides, has no YAML key: it is populated by a setup's surface_condition (its overrides field), or left at the all-nothing default.
The two surface_setup markers, DefaultMoninObukhov and DefaultExchangeCoefficients, are lightweight placeholders that the config-driven constructor resolves into a concrete flux_scheme against params (a default roughness length or exchange coefficient).
Coupling to an external driver
The coupler still builds a complete AtmosSurface (all four fields are present); the two patterns differ only in the flux_scheme/temperature pair:
- Atmosphere skips surface computation:
flux_scheme = nothing(YAML"PrescribedSurface").update_surface_conditions!early-returns, sotemperatureis never read (leave it at its default).init_sfc_conditions_zero!pre-fills safe defaults at cache-build so RRTMGP / diagnostic EDMF never see uninitialized memory, and the coupler overwritessfc_conditionsdirectly. - Atmosphere computes fluxes from a coupler-supplied SST: a real
flux_scheme(e.g.MoninObukhov(…)) together withtemperature = CoupledTemperature(field). The coupler writesT_sfcintofieldbetween steps; the atmosphere reads it and computes the surface fluxes. Per-cell boundary overrides can be aFields.Field{<:SurfaceBoundaryOverrides}on the cache. Seetest/coupler_compatibility.jl.
Developer Guide
Design: one source of truth
Surface behavior lives entirely on atmos.surface. Principles:
- Orthogonality:
flux_scheme,temperature,boundary_overrides, andsurface_albedoare independent axes. Adding an option on one shouldn't touch the others. - Dispatch over branching: behavior is selected by dispatch on concrete types, not
if/elseifon config strings. - Eager resolution: YAML markers and
Default*placeholders resolve to concrete structs at construction, so the hot path sees only concrete types.
Data flow
The entry point update_surface_conditions! (called from set_precomputed_quantities!) does four things: (1) early-return if isnothing(flux_scheme); (2) resolve the temperature via surface_temperature; (3) resolve the flux scheme via resolve_flux_scheme (once per update); (4) broadcast surface_state_to_conditions over every surface point.
The kernel mixes surface-space and lowest-interior-level values, which live on different spaces, so a normal Field broadcast would error. The code drops to Fields.field_values(...) (raw DataLayouts) so the values broadcast as plain same-shape arrays.
Dispatch chains
Three small families cover all behavior:
surface_temperature (surface_temperature.jl): temperature type → value:
| Type | Returns |
|---|---|
AnalyticTemperature | the struct itself (deferred) |
ExternalTemperature | field_values of the evaluated input |
SlabOceanTemperature | field_values(Y.sfc.T) |
CoupledTemperature | field_values(t.field) |
resolve_T_sfc (surface_conditions.jl): in the per-cell kernel, an AnalyticTemperature is evaluated as t.f(coordinates, surface_temp_params, t_time); scalars and DataLayouts pass through. This two-step design lets analytic formulas see each cell's local coordinates while field-valued temperatures resolve once up front.
Flux scheme → flux specs (in surface_state_to_conditions): branches on ExchangeCoefficients vs MoninObukhov, and within MoninObukhov on whether fluxes are prescribed (HeatFluxes/θAndQFluxes) or derived from roughness.
Constraints
- Scalars must broadcast.
Base.broadcastable(x) = tuple(x)is defined once on the abstract supertypesSurfaceParameterizationandSurfaceTemperature, so every concrete subtype inherits it for free. A new subtype needs nothing extra; the only ways to break this are introducing a parallel hierarchy that isn't a subtype, or removing the supertype method. surface_temperaturereturns aDataLayout, anAnalyticTemperature, or a scalar: nothing else. ReturnFields.field_values(...), not aField. A scalar is permitted — it passes throughresolve_T_sfcunchanged — but no built-in type currently returns one; the four in-tree types return either the struct (AnalyticTemperature) orfield_values(...).- Time-varying fluxes resolve per-update, not per-cell: a
MoninObukhovwith a callablefluxeshas it evaluated once byresolve_flux_scheme, then the resulting numeric scheme is broadcast everywhere. isnothing(flux_scheme)is a supported state: any reader ofatmos.surface.flux_schememust handle it.- Only
SlabOceanTemperatureadds prognostic state:Y.sfcexists only for slab runs, so guardY.sfc.Taccess on that type.
Extending
Both extension points follow the same shape: define a concrete subtype, then add the handful of methods the pipeline dispatches on. Because Base.broadcastable(::SurfaceTemperature) and Base.broadcastable(::SurfaceParameterization) are defined on the abstract supertypes, your subtype inherits broadcastability for free — you do not need to redefine it.
A new temperature source
Define the type as a subtype of
SurfaceConditions.SurfaceTemperature. Store whatever it needs (a function, aField, parameters):struct MyTemperature{F} <: SurfaceConditions.SurfaceTemperature data::F endAdd a
surface_temperaturemethod, the per-update resolver. It must return one of the three broadcastable shapes: a scalar, aFields.DataLayoutof per-cell values, or the struct itself (deferred to the per-cell kernel):# field-valued: resolve once per update SurfaceConditions.surface_temperature(t::MyTemperature, Y, p, t_time) = Fields.field_values(t.data)(Optional) Add a
resolve_T_sfcmethod if you returned the struct in step 2 becauseT_sfcdepends on each cell's coordinates (this is howAnalyticTemperatureworks). It runs inside the broadcast kernel and receives the local coordinates:SurfaceConditions.surface_temperature(t::MyTemperature, Y, p, _) = t # defer SurfaceConditions.resolve_T_sfc(t::MyTemperature, coords, surface_temp_params, t_time) = t.data(coords, surface_temp_params, t_time)(Optional) Wire in prognostic state if
T_sfcshould evolve, mirroringSlabOceanTemperature: add asurface_prognostic_variables(local_geometry, ::MyTemperature)initializer and asurface_kwargs(surface_space, ::MyTemperature)method (soY.sfcis allocated), asurface_temp_tendency!method for the time evolution, and any conservation-diagnostic dispatch indiagnostics/conservation_diagnostics.jl.(Optional) Expose it to configs by extending
AtmosSurface(::AtmosConfig, ...)insrc/config/model_getters.jl(or have a setup return it fromsurface_condition).
A new flux scheme
Define the type as a subtype of
SurfaceConditions.SurfaceParameterization{FT}(the{FT}parameter letsfloat_typerecover the element type):struct MyScheme{FT} <: SurfaceConditions.SurfaceParameterization{FT} coefficient::FT endHandle it in
surface_state_to_conditions: extend theparameterization isa …branch that maps the scheme onto theSurfaceFluxescall (building the appropriateFluxSpecs/SurfaceFluxConfig). This is the one place flux schemes are interpreted.(Optional) Add a
resolve_flux_schememethod if the scheme varies in time, mirroring howMoninObukhovresolves a callablefluxes. It runs once per update (not per-cell) and must return a concrete, time-independent scheme:SurfaceConditions.resolve_flux_scheme(p::MyScheme, t, ::Type{FT}) where {FT} = MyScheme{FT}(p.coefficient * cos(t))(Optional) Expose it to configs/setups as in step 5 above.
Config and cache wiring
AtmosSurface(::AtmosConfig, params, FT; setup_type)(src/config/model_getters.jl) maps YAML keys + setup pieces into a concreteAtmosSurface; setup pieces win via@something.build_cache(src/cache/cache.jl) storesp.sfc_setup = atmos.surface.boundary_overrides(a scalar, or aFieldfor the coupler) and callsinit_sfc_conditions_zero!whenisnothing(flux_scheme).
ClimaAtmos.SurfaceConditions.init_sfc_conditions_zero! — Function
init_sfc_conditions_zero!(p)Zero-initialize p.precomputed.sfc_conditions with safe defaults. Used when the surface flux scheme is nothing (the atmos side does not compute surface conditions) so that the first set_precomputed_quantities! call does not see uninitialized memory in downstream consumers like RRTMGP and diagnostic EDMF.
Debugging checklist
sfc_conditionsNaN/uninitialized under the coupler:init_sfc_conditions_zero!only fires whenisnothing(flux_scheme).T_sfcuniform when it should vary: the temperature must return per-cell values, or be anAnalyticTemperaturewhosefactually readscoordinates.- Space-mismatch error in
update_surface_conditions!: something returned aFieldinstead of aDataLayout/scalar, or a type is missingbroadcastable. Y.sfcnot found: not aSlabOceanTemperaturerun; guard slab-only code.- Time-varying flux not updating:
MoninObukhov.fluxesmust be a callable(t, FT) -> PrescribedFluxes(resolved each update), not a fixedHeatFluxescaptured at construction.