Skip to content

Add composable SlabLand and atmosphere–land coupling#220

Open
kaiyuan-cheng wants to merge 45 commits into
mainfrom
kyc/slabland
Open

Add composable SlabLand and atmosphere–land coupling#220
kaiyuan-cheng wants to merge 45 commits into
mainfrom
kyc/slabland

Conversation

@kaiyuan-cheng
Copy link
Copy Markdown
Collaborator

@kaiyuan-cheng kaiyuan-cheng commented May 8, 2026

Summary

Adds a composable land-surface component to EarthSystemModel and the atmosphere–land coupling that lets Breeze drive it through similarity-theory turbulent fluxes. The default configuration — SlabEnergy + BucketHydrology + ConstantSurfaceProperties — is the classic Manabe-bucket slab in a few hundred lines of core numerics.

Architecture — SlabLand, three closure axes

A SlabLand is a 2D land model on an Oceananigans (Nx, Ny, Flat) grid composed of three pluggable closures along independent axes:

  • energy :: AbstractEnergyBalance — skin temperature
  • hydrology :: AbstractHydrology — water budget + moisture availability β
  • surface :: AbstractSurfaceProperties — albedo / emissivity / roughness

Each closure declares its prognostic state (prognostic_variables) and the flux/forcing fields it consumes (flux_variables); the container assembles the union as a NamedTuple, so a pure thermal slab carries state = (T,) only and the bucket adds the water-budget fields it actually needs. Atmosphere-facing accessors (surface_temperature, surface_wetness, albedo, emissivity, momentum_roughness_length, scalar_roughness_length) dispatch through the closures.

Per step the container calls step!(energy, …) then step!(hydrology, …), refreshes diagnostics via update_diagnostics! in hydrology → surface → energy order, and fills state halos. Closures read each other's state through the shared NamedTuple but never call each other directly.

Default closures

  • SlabEnergy — single-temperature slab energy balance with areal heat capacity ρ c H (J m⁻² K⁻¹).
  • BucketHydrology — Manabe (1969) bucket with field capacity W_max and critical wetness W_crit / W_max; produces the moisture-availability factor β consumed by the latent-heat BC.
  • ConstantSurfaceProperties — spatially uniform albedo / emissivity / momentum and scalar roughness lengths.
  • DryLand (β ≡ 0) and SaturatedSurface (β ≡ 1) — stub hydrologies for tests and idealized runs.

Atmosphere–land coupling

AtmosphereLandModel mirrors AtmosphereOceanModel. A new atmosphere_land_interface runs the similarity-theory turbulent-flux pipeline:

  • reads the slab's T, moisture_availability (β), and (optionally) roughness_length through the ComponentExchanger;
  • computes sensible / latent / water-vapor / momentum fluxes plus u★, θ★, q★;
  • writes back into the slab's flux accumulators — net_energy_flux = −(𝒬ᵀ + 𝒬ᵛ) (positive into the slab), and the positive/negative parts of the vapor flux into precipitation / evaporation.

Atmosphere flux drivers now dispatch on the interface type, so adding new surface components doesn't require touching the ocean/sea-ice paths. A 3D Breeze + SlabLand example over a homogeneous slab is included (examples/breeze_over_slab_land.jl).

Known limitations (follow-ups)

  • Land radiation (albedo / emissivity → SW / LW) is not yet plumbed through the Radiation component.
  • When both ocean and land interfaces are present, the Breeze bottom-BC assembler overwrites rather than tile-fraction blending.
  • Direct-Breeze-BC integration.
  • Heterogeneous land-use / vegetation-class registry to return alongside richer closures.

kaiyuan-cheng and others added 8 commits May 6, 2026 20:39
Ports the slab-compatible physics of the RUC land-surface scheme
(Smirnova et al. 1997, 2016) into NumericalEarth as a single-layer
prognostic land model, plus a USGS 24-class lookup table for
populating per-cell vegetation Fields.

Physics included (slab-only subset):

  * Snow column: prognostic SWE, Anderson (1976) compaction,
    Robinson-Kukla (1985) albedo blending, Koren et al. (1999) /
    Niu-Yang (2007) snow-cover fraction, sublimation, melt with
    liquid retention, continuous albedo aging.
  * Canopy: Beer-Lambert interception (rain + snow), wet-canopy
    direct evaporation, two-source canopy temperature
    (Deardorff 1978).
  * Soil: Manabe (1969) / ISBA single-bucket θ, Mahfouf-Noilhan
    (1991) β = mavail, freeze/thaw with phase-change latent heat.
  * Vegetation: Jarvis-Stewart (Jarvis 1976; Stewart 1988)
    canopy resistance r_s; Sellers et al. (1992) bare-soil r_g.
  * Implicit skin-T solver (`vilka`) using Buck (1981) closed-form
    q_sat in place of f90 lookup table.

Files:

  * `src/Lands/ruc_slab_land.jl` — `RucSlabLand`, `RucSlabLandParameters`,
    13-step `time_step!`, `update_state!`, exchanger / checkpointing
    hooks.
  * `src/Lands/usgs_land_classification.jl` — `LandClassification`,
    `usgs_land_classifications(FT)` (24-class registry mirroring
    WRF's VEGPARM.TBL RUC entries), `apply_land_classifications!`.
  * `src/Lands/Lands.jl` — wire the new files and exports.
  * `src/NumericalEarth.jl` — top-level re-exports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plug land-surface models (currently RucSlabLand) into the existing
EarthSystemModel similarity-theory turbulent-flux pipeline, paralleling
AtmosphereOceanModel. The convenience constructor

    model = AtmosphereLandModel(atmosphere, land)

builds an EarthSystemModel with no ocean / sea-ice and a new
atmosphere-land interface that computes (sensible, latent, momentum)
fluxes via SimilarityTheoryFluxes using land-side T_g, β = mavail, and
a scalar grass-style roughness (z0_m = 0.1 m by default).

  * `interface_states.jl` — `BetaSurfaceSpecificHumidity`:
    `q_s = β · q_sat(T_g)`. β is threaded through the iteration's
    InterfaceState.S slot, so the existing fixed-point solver carries
    it through without API changes downstream.

  * `atmosphere_land_fluxes.jl` (new) — `AtmosphereLandFluxes`,
    `atmosphere_land_interface(...)`, `compute_atmosphere_land_fluxes!`,
    and a kernel mirroring the atmosphere-ocean one but reading
    surface state from the land exchanger.

  * `component_interfaces.jl` — extended `ComponentInterfaces` with a
    fourth interface field (`atmosphere_land_interface`); kwargs and
    defaults `default_al_specific_humidity`,
    `default_atmosphere_land_fluxes` (scalar z0).

  * `components.jl` — `exchange_grid` falls back to `land.grid` when
    only atmosphere + land are present.

  * `earth_system_model.jl` — `AtmosphereLandModel` constructor and an
    ocean-less `default_nan_checker` fallback that watches the land
    surface temperature.

  * `time_step_earth_system_model.jl` — wire
    `compute_atmosphere_land_fluxes!` into `update_state!`.

  * `Lands/ruc_slab_land.jl` — `update_net_fluxes!(::ESM, ::RucSlabLand)`
    consumes interface fluxes and writes `temperature_flux` and
    `forcings.moisture_flux` for the slab's `time_step!`.

  * `ext/NumericalEarthBreezeExt/breeze_atmosphere_interface.jl` —
    `update_net_fluxes!(::ESM, ::BreezeAtmosphere)` now also pushes
    atmosphere-land fluxes into Breeze's bottom flux BCs.

  * `atmosphere_ocean_fluxes.jl`, `atmosphere_sea_ice_fluxes.jl`,
    `sea_ice_ocean_fluxes.jl` — guard against `nothing` interfaces so
    AtmosphereLandModel (which has no ao/asi/sio interfaces) walks
    through `update_state!` cleanly.

  * `EarthSystemModels.jl`, `NumericalEarth.jl` — `AtmosphereLandModel`
    + `BetaSurfaceSpecificHumidity` exports; `NoLandInterface` alias
    and corresponding `compute_atmosphere_land_fluxes!` no-op
    dispatches for ocean-only / sea-ice-only / no-interface models.

  * `examples/breeze_over_slab_land_3d.jl` — replaces the prior manual
    bulk-formula callback with a one-line AtmosphereLandModel
    construction.

Known v1 limitations (deferred follow-ups):
  * Roughness length is a single scalar baked into the flux
    formulation; the slab's per-cell time-varying `znt` field is not
    yet read by the kernel.
  * Land radiation (albedo / emissivity → shortwave / longwave) is
    not yet plumbed through the `Radiation` component.
  * When both ocean and land interfaces are present, the Breeze
    bottom-BC assembler overwrites rather than tile-fraction blending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings RucSlabLand closer to the operational RUC LSM in three areas
(soil moisture availability, snowmelt retention, canopy interception)
and wires the atmosphere state into the slab's auxiliary forcings on
each coupled step so the bare-soil resistance sees the current air q.

  * `src/Lands/ruc_slab_land.jl`:
    - Replace Mahfouf-Noilhan β with RUC `mavail` (residual / (θ_fc -
      θ_air_dry), blended with snow cover) and add `soilres`, the
      RUC bare-soil evaporation limiter that gates bare-soil
      evaporation against the q_a / q_g comparison and a fc-fraction
      cosine-squared cutoff. `_step_soil_moisture!` now scales the
      bare-soil evap term by `soilres` instead of `mavail`.
    - `_update_mavail_rs_rg!` reads `T`, `p_surf`, `snowfrac`, and
      `theta_air_dry` to compute both fields; new `soilres` Field is
      added to the vegetation NamedTuple, defaulted to 1, and
      published through the ComponentExchanger.
    - Snow melt rewritten: `_melt_snow!` now applies the RUC Egglston
      melt cap (active for cold or low-density packs below 283 K) and
      the Koren-style retained-melt split, mixing retained liquid back
      into `rhosn` and clamping. `_drain_swl!` reduced to a pre-pass
      that drains carried-over `swl` above capacity; melt-step
      retention/overflow is computed inline.
    - Canopy interception uses the RUC fixed `sat = 5e-4 m` (new
      `canopy_water_capacity` parameter) instead of the
      0.2 mm·LAI·vegfrac formula.
    - New parameters: `canopy_water_capacity`, `meltfactor`,
      `snow_retention_min_frac`, `snow_retention_max_frac`,
      `snow_retention_depth_scale`, `snow_retention_depth_factor`.
      Step ordering in `time_step!` updated so the carried-over
      drain runs before melt.

  * `src/EarthSystemModels/time_step_earth_system_model.jl`:
    `sync_atmosphere_land_auxiliary_forcings!` copies the atmosphere
    exchanger's `T`/`q` into `land.forcings.air_temperature` /
    `air_humidity` between the atmosphere and land interpolation
    passes, so RucSlabLand's diagnostic update sees the current
    atmospheric state. No-ops when either component is `nothing`
    or the land model lacks those forcing fields.

  * `Project.toml`: promote Breeze from weakdeps to deps and add
    CUDA + CairoMakie compat entries for the new example/test path.

  * `examples/breeze_over_ruc_slab_land.jl`: new 3D Breeze + RucSlabLand
    convection example over heterogeneous USGS land use, replacing
    the prior `breeze_over_slab_land_3d.jl`.

  * `test/test_ruc_slab_land.jl`: new unit tests for the slab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snow density compaction, melt, and sublimation now operate on total
pack SWE (`snwe + swl`) so the split solid/liquid Julia state yields
the same bulk density and snow height as RUC's combined `snwe`.
Fresh-snow density follows the Smirnova `tanh(T_air)` formula and the
melt limiter is reactivated whenever current-step snowfall is
non-zero. Drops parameterised `snhei_crit*`, computing them from local
snow density inline. USGS classification defaults (LAI, z0, snow/ice
emissivity) realigned with WRF, and an `is_urban` mask is propagated
to the canopy state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the seven physics fixes catalogued in the slab-vs-Fortran audit
and prune two diagnostics with no downstream consumer:

- warm-pack vapor flux now applied at all temperatures (closes a snow-
  mass conservation gap when T_g > 273.15 K with snowfrac > 0)
- soil freeze/thaw uses Clapp-Hornberger / Clausius-Clapeyron unfrozen-
  water equilibrium instead of binary phase change; adds psi_sat, bclh
- post-melt density blend uses step-local retained liquid only
- canopy drip routes entirely to the snow pack when snowfrac >= 0.75
- snowfracnewsn evaluated from pre-increment snowfallac (Fortran:1641)
- snhei_crit_newsn uses pre-compaction rhosn snapshot (Fortran:1419)
- drop the bare-soil r_g (Sellers 1992) field and its parameters - the
  exchanger exposed it but no atmosphere-land coupler consumed it
- drop the snow-albedo aging prognostic (alb_snow_local + tau_cold/warm
  decay) in favour of Fortran's single alb_snow end-member; the warm-T
  Robinson-Kukla attenuation remains as the only albedo decay path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The flux-driven path (T_g -= Jᵀ·Δt/H) is the only skin update wired
through the AtmosphereLandModel coupler; vilka was never called and
its d1/d2 forcing fields were never written. Also remove the
write-only `rhosnfall` diagnostic and unused ConstantField/ZeroField
imports left over from earlier iterations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- jarvis_resistance now takes the local surface pressure (hPa) from forcings
  instead of hardcoding 1013.25 hPa, so q_sat(T_air) reflects the actual
  surface pressure used elsewhere in the LSM.
- apply_land_classifications! reads existing vegetation fields as its
  starting buffer instead of zeroing them, so cells with unrecognised USGS
  ids keep their previously assigned values.
- Tests cover both behaviours; example comments and strip z0/LAI defaults
  updated to match the current scalar-roughness coupling path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 8, 2026 22:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a prognostic, single-layer RUC-like slab land-surface model (RucSlabLand) and connects it to the existing EarthSystemModel coupling pipeline via a new atmosphere–land turbulent-flux interface (AtmosphereLandModel). It also adds a USGS 24-class land-classification registry and an end-to-end Breeze + land coupling example over heterogeneous land use.

Changes:

  • Add RucSlabLand + parameters, snow/canopy/soil/vegetation physics, and USGS land-classification table + applicator.
  • Add atmosphere–land interface flux computations (MOST + β-reduced surface humidity) and an AtmosphereLandModel convenience constructor.
  • Wire Breeze bottom boundary flux assembly to support the new atmosphere–land interface and add tests + a 3D example.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/test_ruc_slab_land.jl Adds unit tests for key RUC-slab process parameterizations and MOST roughness coupling.
src/NumericalEarth.jl Exports newly added land and coupling APIs.
src/Lands/usgs_land_classification.jl Implements USGS 24-class registry and applies categorical maps to per-cell vegetation fields.
src/Lands/ruc_slab_land.jl Implements the prognostic RUC-like slab land model and its ESM coupling hooks.
src/Lands/prescribed_land.jl Explicitly disables atmosphere–land interface for PrescribedLand.
src/Lands/Lands.jl Wires new land files into the Lands module and exports new land APIs.
src/EarthSystemModels/time_step_earth_system_model.jl Calls atmosphere–land flux computation and syncs some atmosphere fields into land forcings.
src/EarthSystemModels/InterfaceComputations/similarity_theory_turbulent_fluxes.jl Adds support for per-cell land roughness lengths in similarity theory.
src/EarthSystemModels/InterfaceComputations/sea_ice_ocean_fluxes.jl Adds a nil-guard for missing sea-ice–ocean interface.
src/EarthSystemModels/InterfaceComputations/roughness_lengths.jl Introduces LandRoughnessLength.
src/EarthSystemModels/InterfaceComputations/InterfaceComputations.jl Exports/includes new atmosphere–land flux machinery and β humidity formulation.
src/EarthSystemModels/InterfaceComputations/interface_states.jl Adds BetaSurfaceSpecificHumidity formulation.
src/EarthSystemModels/InterfaceComputations/compute_interface_state.jl Threads interior properties through interface-state iteration.
src/EarthSystemModels/InterfaceComputations/component_interfaces.jl Adds atmosphere_land_interface creation and default land flux/humidity formulations.
src/EarthSystemModels/InterfaceComputations/coefficient_based_turbulent_fluxes.jl Updates signature to accept interior properties (for consistency).
src/EarthSystemModels/InterfaceComputations/atmosphere_sea_ice_fluxes.jl Adds nil-guard for missing atmosphere–sea-ice interface.
src/EarthSystemModels/InterfaceComputations/atmosphere_ocean_fluxes.jl Adds nil-guard for missing atmosphere–ocean interface.
src/EarthSystemModels/InterfaceComputations/atmosphere_land_fluxes.jl New file computing atmosphere–land MOST fluxes with land roughness + β humidity.
src/EarthSystemModels/EarthSystemModels.jl Exports new coupling types and extends “no-interface” dispatch.
src/EarthSystemModels/earth_system_model.jl Adds AtmosphereLandModel constructor and updates NaN checking fallback.
src/EarthSystemModels/components.jl Extends exchange_grid defaults to support land-only coupled models.
Project.toml Adds Breeze/CUDA/CairoMakie dependencies and compat entries.
ext/NumericalEarthBreezeExt/breeze_atmosphere_interface.jl Updates Breeze net-flux assembly to optionally use ocean and/or land interface fluxes.
examples/breeze_over_ruc_slab_land.jl New 3D heterogeneous land-use Breeze + RUC slab land coupling example.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/EarthSystemModels/components.jl
Comment thread src/EarthSystemModels/EarthSystemModels.jl Outdated
Comment thread src/Lands/ruc_slab_land.jl Outdated
Comment thread src/EarthSystemModels/time_step_earth_system_model.jl Outdated
@kaiyuan-cheng kaiyuan-cheng marked this pull request as draft May 8, 2026 23:07
kaiyuan-cheng and others added 4 commits May 9, 2026 01:39
Refactor the monolithic RucSlabLand land-surface component into a
generic SlabLand container composed of three pluggable closures —
energy balance, hydrology, and surface properties — per the
slab-land-design.md spec. Each closure declares its prognostic
state and incoming flux fields; the container assembles the union.

Default trio (SlabEnergy + ManabeBucket + ConstantSurfaceProperties)
gives the classic Manabe-bucket slab in a few lines. RUC physics is
preserved verbatim in three concrete closures (RucEnergy, RucHydrology,
RucSurfaceProperties) — every kernel body diffs byte-identically
against the pre-refactor source. RucSlabLand is now a convenience
constructor returning a SlabLand with the RUC trio, so existing call
sites and ESM coupling glue keep working unchanged.

Also adds DryLand and SaturatedSurface stubs (β ≡ 0 / 1) and the
atmosphere-facing accessors (surface_temperature, surface_wetness,
albedo, emissivity, momentum_roughness_length, scalar_roughness_length)
described in design doc §6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`CUDA.functional()` itself can throw on an unsupported compute
capability (e.g. CUDA 13+ on a TITAN V, where the driver reports
ERROR_UNKNOWN during context retention). Wrap the per-example
`reclaim` calls in try/catch so the docs build keeps going on
runners with unsupported GPUs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documenter's TextDiff.makediff! is a recursive LCS reconstruction
whose depth scales with the token count of the diff. When a doctest
fails with a multi-thousand-token output (e.g. a long error
stacktrace), the recursion overflows the default ~4 MB task stack
and aborts the docs build before the actual doctest error is
reported. Override with a behaviorally-equivalent iterative version
in docs/make.jl so the diff completes and the underlying failure
surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NumericalEarth's main Project.toml now requires CUDA 6.1.0+ and
allows ClimaSeaIce 0.4.4-0.5, but test/Project.toml was still
pinning CUDA = "5.9.5" and ClimaSeaIce = "0.4.10". Pkg test was
unable to resolve a manifest because the test compat excluded the
versions required by NumericalEarth. Match the main project's
compat entries so the test environment can be resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kaiyuan-cheng and others added 2 commits May 11, 2026 14:13
The prior bump of NumericalEarth's CUDA compat to "6.1.0" forced
the test sandbox to upgrade CUDA to 6.x, but Reactant 0.2.255
(pinned by test/Project.toml for Reactant CI) is not compatible
with CUDA 6 at runtime, causing the test_reactant worker to crash
with Malt.TerminatedWorkerException after ~3 minutes.

Widen the main project's CUDA compat to "5.9.5, 6" so both major
versions are acceptable, and revert test/Project.toml's CUDA pin to
"5.9.5" so the Reactant CI job resolves the known-good CUDA 5.9.5
that ships compatible binaries for Reactant 0.2.255. Leave
ClimaSeaIce at "0.4.4, 0.5" in both projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rename parameter struct fields (theta_wilt → theta_wilting_point, bclh
→ clapp_hornberger_exponent, alb/emiss/z0_* → albedo/emissivity/
roughness_length_*, sncovfac → snow_cover_scale_factor, rhosn_min/max
→ snow_density_min/max, r_smin/max → stomatal_resistance_min/max,
rg_lim → solar_radiation_limit, vpd_lim → vapor_pressure_deficit_limit,
T_opt → temperature_optimum, *_frac → *_fraction, etc.). Also rename
the public ComponentExchanger keys (alb/emiss/znt → albedo/emissivity/
roughness_length, mavail → moisture_availability, r_s → stomatal_
resistance, soilres → soil_resistance) so the coupler interface reads
in plain English. LandClassification.r_smin / .z0 kept (mirror
VEGPARM.TBL column names). All 55 RUC slab-land tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread examples/breeze_over_ruc_slab_land.jl Outdated
Comment thread examples/breeze_over_ruc_slab_land.jl Outdated
Comment thread src/EarthSystemModels/InterfaceComputations/atmosphere_ocean_fluxes.jl Outdated
Comment thread src/Lands/energy_balance/ruc_energy.jl Outdated
Comment thread src/Lands/energy_balance/ruc_energy.jl Outdated
Comment thread src/Lands/surface_properties/constant_surface_properties.jl Outdated
Replace isnothing guards in compute_atmosphere_{ocean,sea_ice,land}_fluxes!
with method dispatch on the interface argument, addressing review feedback
on the new AtmosphereLandModel path. Inlines the local interface binding
to drop repeated `coupled_model.interfaces.<x>_interface.<field>` accesses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@glwagner glwagner added the land 🏔️ the solid part label May 11, 2026
Comment thread src/Lands/hydrology/manabe_bucket.jl Outdated
Comment thread src/Lands/surface_properties/ruc_surface_properties.jl Outdated
kaiyuan-cheng and others added 3 commits May 12, 2026 05:48
Also renames the cached wetness field from `β` to
`moisture_availability` for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaiyuan-cheng kaiyuan-cheng changed the title Add RUC-like slab land model and atmosphere-land coupling Add composable SlabLand and atmosphere–land coupling May 12, 2026
kaiyuan-cheng and others added 7 commits May 12, 2026 15:43
Strip the unused SlabLandParameters scaffolding, collapse the
duplicate ClimaSeaIce compat in docs/Project.toml, revert the
unrelated TextDiff/CUDA.reclaim workarounds in docs/make.jl, and
drop the redundant per-NoXInterfaceModel no-op flux methods now
that compute_*_fluxes! guards `::Nothing` interfaces internally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Merge the three slab-land flux-assembly kernels into one
  `_assemble_slab_land_fluxes!` that writes Q/P/E in one launch,
  skipping fields that aren't present via `_maybe_write!`.
- Drop unused `NoSeaIceInterface{,Model}`, `NoAtmosInterface{,Model}`,
  `NoLandInterface{,Model}` type aliases.
- Make `BucketHydrology` truly stateless — the cached
  `moisture_availability` field on the struct was never read.
- Default `default_atmosphere_land_fluxes(::Nothing, FT)` to
  `nothing` so a land-less model doesn't allocate a SimilarityTheory
  closure.
- Hoist `Time(clock.time)` out of the radiation-state call site;
  fix a stray `+` in the `radiation` docstring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the ocean/sea-ice design: radiative surface properties for
land now live on `radiation.surface_properties.land`, not on the
SlabLand surface-properties closure.

- Add `air_land_interface_radiation_state` with the `(::Nothing,
  ::Nothing)` fallback in InterfaceComputations and the concrete
  `PrescribedRadiation`-aware method in Radiations (reads α and ε
  from `rk.surface_properties.land`).
- Wire `radiation`, `radiation_kernel_props`, and the radiation
  exchanger state into `compute_atmosphere_land_fluxes!` exactly
  like the ocean/sea-ice path; the kernel calls the new function
  instead of the hardcoded `(nothing, nothing)` stub.
- Extend `present_surfaces(ocean, sea_ice, land)` to include `:land`
  so `allocate_interface_fluxes!` allocates a per-cell radiation
  flux buffer for land.
- Drop `albedo`/`emissivity` from `AbstractSurfaceProperties` and
  `ConstantSurfaceProperties`; the closure is now aerodynamic-only
  (roughness lengths). Update docs and the slab-land example.

Verified with two CPU smoke tests: `radiation = nothing` falls back
cleanly; `PrescribedRadiation(; land_surface = …)` allocates the
land flux buffer and steps without NaN.

Follow-up needed for the physical loop to close: an
`apply_air_land_radiative_fluxes!` kernel and consumer in
`_assemble_slab_land_fluxes!` so net radiation drives the slab
energy budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaiyuan-cheng kaiyuan-cheng marked this pull request as ready for review May 12, 2026 19:07
kaiyuan-cheng and others added 6 commits May 13, 2026 18:46
Diagnostics uses NoSeaIceInterfaceModel (added in #226) for ZeroField
fallbacks on frazil/sea-ice-ocean fluxes; it was dropped on this branch
in 73a9b7a as unused, but became load-bearing after the merge from main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Veros upstream pins requests to <=2.34.0 in its pyproject; pixi was
resolving requests to 2.34.1 (the latest on PyPI), causing
install_veros() to fail with a pypi resolution conflict in CI. Pre-pin
requests before adding veros so the resolver picks a compatible
version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Veros upstream bumped requests from 2.34.0 to 2.34.2 in
team-ocean/veros 77baf7b, which resolves the pixi conflict naturally.
The pin from c33b41b is now too restrictive — pixi rejects it when
another dep wants requests>=2.34.1. Drop the pin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
glwagner and others added 10 commits May 24, 2026 10:05
Addresses code-review findings on the SlabLand branch:

- Introduce an `AbstractLand` supertype; `SlabLand` and `PrescribedLand`
  subtype it so coupling methods can dispatch on a shared root.
- Move bucket `moisture_availability` (β) recomputation out of `step!`
  and into `update_diagnostics!`, where it belongs as a diagnostic of
  `W`. Keep β in the single `state` namedtuple per convention; document
  it as a diagnostic-shaped state entry.
- Unify `AtmosphereOceanFluxes` and `AtmosphereLandFluxes` into a single
  `AtmosphereSurfaceFluxes` container; the two interfaces shared all
  eight fields and identical adapt/architecture methods.
- Collapse three near-identical `air_*_interface_radiation_state`
  functions into one `_surface_radiation_state` core with thin
  per-surface wrappers; preserve the optional-`:land` zero fallback.
- Wire prescribed atmospheric rainfall (`Jʳⁿ` from the atmosphere
  exchanger) into `land.fluxes.precipitation` so prescribed-atmosphere
  runs actually rain on the bucket; condensation/dew still accumulates
  alongside.
- Tighten the property-provider contract: `normalize_property(FT, _)`
  at construction time, `property_value(prop, i, j, k=1)` inside
  kernels. Closures no longer create `ConstantField`s every step.
- Drop the `SlabEnergy.heat_capacity` backwards-compat alias (the
  branch is unreleased) and the singular `roughness_length` exchanger
  export plus its `LandRoughnessLength` fallback. Use the explicit
  momentum/scalar pair throughout.
- Fix the `BucketHydrology` β degenerate case: `W_cap ≤ 0` (inactive
  tile / no storage) now returns β = 0, not 1.
- Add a docstring to `InterfaceState` explaining the `S` slot's
  surface-dependent meaning (ocean salinity vs. land β).
- Document `time_step!`/`update_state!` clock + ordering semantics.

Tests
-----

- `test/test_slab_land.jl`: SlabLand energy/hydrology stepping, property
  providers, flux assembly sign conventions (including the new
  prescribed-rainfall path), and atmosphere--land radiation coupling.
- `test/test_radiations.jl`: a new test exercises land surface
  radiative properties on `PrescribedRadiation`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A coarse-resolution (0.25°, 20 × 20 cells) regional land-only
simulation over northwest Wyoming and eastern Idaho — Salt Lake City
to Yellowstone, Pocatello to Jackson. Forced by `JRA55PrescribedAtmosphere`
and `JRA55PrescribedRadiation` (with explicit land albedo/emissivity)
through `AtmosphereLandModel`. The terrain itself isn't resolved by
the slab, but JRA55's native topography drives strong spatial
gradients in T₂ₘ, radiation, and precipitation across the region, so
LST varies interestingly along the Snake River Plain / Wasatch /
Yellowstone Plateau transect.

A 12-hour smoke run confirms diurnal physics: T spans 264-276 K with
a 10 K spatial spread, β stays at the initial 0.66, and Q swings
between night cooling (-300 W m⁻²) and day heating (+180 W m⁻²).
30 days is ~45 s wall time on CPU.

`JRA55PrescribedRadiation` was passing extra surface kwargs through
`other_kw` to `JRA55FieldTimeSeries`, which rejects them. Add explicit
`land_surface` and `snow_surface` kwargs that forward to
`PrescribedRadiation`, so land-only configurations can actually
provide land albedo/emissivity.

Tighten the property-provider error path (`normalize_property` and
`property_value` now throw `ArgumentError` for unsupported types
instead of silently passing them through) and add `Number` assertions
in the SlabLand tests to lock in the construction-time normalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `examples/jra55_forced_slab_land.jl` with `era5_forced_slab_land.jl`,
forced by ERA5 hourly single-level fields (T₂ₘ, dewpoint, 10-m wind,
surface pressure, total precipitation, downwelling SW/LW) over the
Greater Yellowstone / Snake River Plain region (40.5–45.5 °N,
-114 to -109 °W, 0.25° = 20 × 20 cells, 7 days at hourly cadence).

Convert ERA5's two accumulated fields to instantaneous rates inline:
total precipitation in m/hr → kg m⁻² s⁻¹, and downwelling SW/LW from
J m⁻² (accumulated over the past hour) → W m⁻². `PrescribedRadiation`
reads SW/LW as W m⁻² — passing the raw accumulated values is 3600×
too large and was the root cause of the run-blowing-up behaviour we
hit while bringing this up. Also convert 2-m dewpoint + surface
pressure to specific humidity with Magnus's formula since ERA5
single levels don't expose q directly.

Pad the BoundingBox by one ERA5 cell on every side so the exchange-grid
halo nodes lie inside the regional cutout (JRA55 sidestepped this by
being global). Use a degenerate single-cell Bounded z on the land grid
(rather than `Flat`) so the radiation regridder's `FractionalIndices`
gets a numeric z, and explicitly fill `parent` of the land state so
halo cells are initialised before the first flux solve.

Switch `compute_atmosphere_land_fluxes!` to launch over the interior
(`:xy`) rather than `interface_kernel_parameters` (0:Nx+1, 0:Ny+1).
The halo-inclusive iteration exists for the ocean's benefit — `SlabLand`
takes `:xy` interior-only steps and doesn't need halo fluxes.

Register the example in `docs/make.jl` with `build_always = true` so it
builds on every commit; the existing docs workflow already wires
`CDSAPI_KEY` from secrets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve conflicts with main's explicit-imports refactor (#276) and the
pressure-based surface specific humidity (#223):

- `src/Radiations/air_sea_interface_radiation_state.jl`: keep our refactor
  into one `_surface_radiation_state` helper + three per-surface
  wrappers, and apply main's `EarthSystemModels.InterfaceComputations.`
  qualification at each extension site. Keep the optional-`:land`
  zero-radiation fallback.
- `src/EarthSystemModels/InterfaceComputations/component_interfaces.jl`:
  keep the unified `AtmosphereSurfaceFluxes` (vs main's split
  `AtmosphereOceanFluxes` it derived from) and apply main's
  `Oceananigans.Architectures.on_architecture` qualification.
- `src/EarthSystemModels/earth_system_model.jl`: keep the land NaN
  checker fallback; apply main's `Oceananigans.Diagnostics.default_nan_checker`
  qualification.
- `src/EarthSystemModels/time_step_earth_system_model.jl`: keep
  `compute_atmosphere_land_fluxes!` in the `using` block and the
  `apply_air_land_radiative_fluxes!` call in `update_state!`.
- `src/Lands/Lands.jl`: move to main's explicit-imports style. Keep
  `AbstractLand` as the marker abstract type for prognostic land
  components (SlabLand). PrescribedLand stays under
  `AbstractPrescribedComponent` (main's choice).
- `src/Lands/prescribed_land.jl`: subtype `AbstractPrescribedComponent`
  (main); keep our `atmosphere_land_interface(... ::PrescribedLand ...)
  = nothing` to match the new atmosphere–land path.
- `src/NumericalEarth.jl`: keep main's reorganised export block; add
  our new SlabLand-side names (AbstractLand, SlabLand, SlabEnergy,
  BucketHydrology, DryLand, SaturatedSurface, ConstantSurfaceProperties,
  surface_temperature, surface_wetness).
- `src/Oceans/slab_ocean.jl`: keep main's qualified extensions; keep
  our `exchange_grid(... ; land=nothing)` signature.
- `src/Radiations/Radiations.jl`: move to main's `using ..NumericalEarth`
  / `using ..EarthSystemModels.InterfaceComputations` imports; add
  `air_land_interface_radiation_state` to the import list so
  `apply_air_land_radiative_fluxes!` can call it.
- `src/Radiations/apply_air_land_radiative_fluxes.jl`: rewrite to the
  new pattern — relative imports, `EarthSystemModels.apply_air_land_radiative_fluxes!`
  extensions, `(::EarthSystemModel{<:Nothing}) = nothing` fast path.

`BetaSurfaceSpecificHumidity` (`src/EarthSystemModels/InterfaceComputations/interface_states.jl`)
is rewritten on top of main's new pressure-based formulation. The
β-blend is now:

    qₛ = qₐ + β · (qₛ_sat(Tₛ, p) − qₐ),    qₛ_sat from main's Raoult/p formula.

To carry qₐ into the iteration we extend `surface_specific_humidity`
with an optional 6th positional `qᵃᵗ` (defaults to zero, ignored by
the impure-saturation variant); `compute_interface_state.jl` threads
`atmosphere_state.q` through, and `_compute_atmosphere_land_interface_state!`
passes `qᵃᵗ` into the initial-state evaluation. Verified end-to-end
by re-running `examples/era5_forced_slab_land.jl` post-merge — same
T/W/Q trajectory as before the merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`local_roughness_lengths` calls `local_roughness_length(ℓ, props, Val(:momentum|:scalar))`
for every formulation (ocean and land). After the SlabLand-side
`LandRoughnessLength` method was added with the 3-arg `Val` signature,
the ocean's `MomentumRoughnessLength`/`ScalarRoughnessLength` still
only had the 2-arg form and the dispatch fell through:

    MethodError: no method matching local_roughness_length(
        ::MomentumRoughnessLength{Float64,...},
        ::@NamedTuple{reference_density, heat_capacity, ...},
        ::Val{:momentum})

This broke the existing `single_column_os_papa_simulation` docs build.
Add a `local_roughness_length(ℓ, interior_properties, ::Val) = ℓ`
fallback so non-land formulations ignore the surface kind and return
themselves unchanged. Verified both dispatches (ocean returns
formulation unchanged; land reads the per-cell roughness field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite `examples/era5_forced_slab_land.jl` at ~1 km resolution
(200 × 200 cells, 43.25-45.25 °N × -111 to -109 °W) with ETOPO 2022
elevation-driven downscaling so terrain is visible in the skin
temperature field:

* Reduce the box to 2° × 2° centred on Yellowstone, the Tetons, Jackson
  Hole, the Madison Valley and the northern Wind Rivers.
* Load ERA5 single-level fields onto the 1 km land grid directly via
  `FieldTimeSeries(metadata, land_grid)` — backend bilinearly downscales.
* Regrid ETOPO 2022 onto both the 1 km land grid and a 0.25°
  ERA5-effective grid; project the ERA5 elevation back to 1 km by
  per-cell lookup, then apply an environmental lapse correction
  `T_local = T_ERA5 − Γ · Δz` with Γ = 6.5 K km⁻¹, plus a hydrostatic
  surface pressure adjustment and a recomputed `q` from dewpoint at the
  corrected (T, p). Δz spans roughly ±1.5 km in this domain, so the
  lapse signal alone produces ~13 K skin-T contrast between the Snake
  River Plain floor and the Wind River summits.
* Shorter 3-day window keeps the run tractable.
* Cleanup hooks (close JLD2 writer, drop atmosphere FTS references,
  GC) before the animation block — at 200 × 200 the in-process working
  set is large enough that some Julia startups segfault before Makie
  renders. The Literate.jl sandboxed execution used by the docs build
  is unaffected.

Architectural note in the docstring acknowledges that the atmosphere
FTS lives on the 1 km land grid rather than ERA5 native, which makes
the state-exchanger interpolation an identity; a proper downscaling
exchanger that does the bilinear regrid plus elevation correction
belongs in `EarthSystemModels.InterfaceComputations`. Treating the
downscaling as input pre-processing keeps the example self-contained
in the meantime.

Pin Oceananigans to `main` via `[sources]` in `Project.toml` and
`docs/Project.toml` (the bilinear `set!` improvements live there).
Rename internal roughness-length names to `ℓᵐ`/`ℓˢ` in the atmosphere–
land flux helpers; add matching notation entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve two conflicts from PR #269 (Allow ocean-only sea ice model
without atmosphere):

* `atmosphere_ocean_fluxes.jl` and `atmosphere_sea_ice_fluxes.jl` —
  adopt main's helper-function refactor (`atmosphere_ocean_fields`,
  `atmosphere_ocean_data`, `atmosphere_ocean_properties`,
  `atmosphere_ocean_radiation_state` and the sea-ice equivalents). The
  per-component `compute_atmosphere_*_fluxes!` functions are now single-
  argument and read the interface from `coupled_model.interfaces.*`.
* `EarthSystemModels.jl` — add the `NoAtmosInterface` / `NoAtmosInterfaceModel`
  aliases main introduced, plus the matching no-op fallbacks for
  `compute_atmosphere_*_fluxes!` and `compute_sea_ice_ocean_fluxes!`.
  Also wire `compute_atmosphere_land_fluxes!` into the NoAtmos / NoInterface
  fallbacks so a land-only earth system without atmosphere short-circuits
  cleanly. The existing 2-arg `compute_atmosphere_land_fluxes!` keeps the
  per-interface Nothing dispatch intact.

Verified the package loads cleanly post-merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. **Atmosphere-Land radiation coupling** (`test/test_slab_land.jl:261, :267`).
   My `compute_atmosphere_land_fluxes!(::NoInterfaceModel) = nothing`
   fallback added in the previous merge fired for `AtmosphereLandModel`
   (ocean/sea_ice/sea_ice_ocean interfaces are all nothing → matches
   `NoInterfaceModel`), suppressing the land flux compute. As a result
   `al_interface.temperature` stayed at 0 K and the radiation kernel
   emitted `σ ε T⁴ = 0`. Drop that specific fallback — the existing
   2-arg `compute_atmosphere_land_fluxes!(coupled_model, ::Nothing)`
   dispatch already handles the case where the AL interface itself
   is nothing.

2. **GPU SlabLand property providers** (`test/test_slab_land.jl:74`).
   The catch-all `property_value(p, i, j, k=1) =
   throw(ArgumentError("…\$(typeof(p))…"))` and the matching
   `normalize_property` fallback introduce a runtime `Symbol`
   allocation that GPUCompiler cannot lower — even when the
   fallback is unreachable from the kernel, its mere presence on
   the dispatch tree trips `gpu__slab_energy_step_with_water!`
   compilation with `unsupported call to ijl_symbol_n`. Drop both
   throwing catch-alls; unsupported types now fail with a plain
   `MethodError` at construction time, which is what we want.

3. **Stale Explicit Imports** in `Lands`. `ExplicitImports` flagged
   five unused imports — `ConstantField`, `FieldTimeSeries`,
   `initialize!`, `interior`, `time_step!`. The first three are
   genuinely unreferenced after recent cleanup. `time_step!` and
   `initialize!` were imported only to extend them, but every
   extension uses the qualified form (e.g.
   `Oceananigans.TimeSteppers.time_step!(::SlabLand, Δt)`), so the
   bare imports are redundant. Trim all five from the Lands `using`
   / `import` block.

All three testsets now pass locally on CPU; the GPU fix is structural
and should let the GPU job get past `property providers` too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…operties

The previous round of fixes passed CPU but GPU still failed on
`SlabLand property providers` because `Field`s adapt to
`OffsetArray{T, 3, CuDeviceArray{T, 3}}` on the device — *not*
`<: AbstractField`. `property_value(p::AbstractField, ...)` then
didn't match and GPUCompiler resolved to a `MethodError` throw it
can't lower. Add `property_value(p::AbstractArray, ...)` so the
GPU-adapted storage matches.

`ForceRestoreEnergy` (the new closure the linter introduced) had two
bugs the new tests caught:

* `_force_restore_step_no_water!` computed `C = Cdry + Cl` instead of
  `C = Cdry` — adding the liquid heat capacity even though no water
  is present. Reduce to just `Cdry`; `_force_restore_step_with_water!`
  remains `Cdry + Cl·W` (correct).
* `stateindex(a::Function, i, j, k, grid, time, (LX, LY, LZ))` used
  `node(...)` which drops Flat-dim entries and returns a shorter
  tuple. On a `(Flat, Flat, Bounded)` grid the `λ, φ, z = node(...)`
  destructuring then crashed with `BoundsError`. Swap to `_node`,
  which always returns the full `(λ, φ, z)` triple with placeholder
  values in Flat dims, so user `(λ, φ, z, t)` callbacks see what
  they expect.

Side cleanup:

* `Lands` brings `stateindex` into scope (used by ForceRestoreEnergy's
  per-cell function/array deep-temperature path).
* `test_slab_land.jl`: import `NumericalEarth.Lands.update_diagnostics!`
  (used by the new LAI/root-depth test) and fix the saturated-bucket
  expected value to account for the LAI canopy-stress multiplier
  `exp(-kᴸ Λ)`, which applies uniformly at all wetness levels.

All `test/test_slab_land.jl` and `test/test_quality_assurance.jl`
testsets pass locally on CPU; the GPU fix is structural and should
let the GPU `SlabLand property providers` testset compile too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

land 🏔️ the solid part

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants