Add composable SlabLand and atmosphere–land coupling#220
Add composable SlabLand and atmosphere–land coupling#220kaiyuan-cheng wants to merge 55 commits into
Conversation
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>
There was a problem hiding this comment.
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
AtmosphereLandModelconvenience 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.
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>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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>
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>
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>
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>
The example was dragging in submodule paths for names that `using NumericalEarth` already re-exports (`PrescribedAtmosphere`, `PrescribedRadiation`, `SurfaceRadiationProperties`, `Metadata`, `BoundingBox`, `MetadataSet`, `SlabLand`, `SlabEnergy`, `BucketHydrology`, `ConstantSurfaceProperties`, `AtmosphereLandModel`, `ETOPO2022`, `regrid_bathymetry`, …) and likewise pulling `Oceananigans.Fields: Center, set!`, `Oceananigans.OutputReaders: FieldTimeSeries`, etc. when `using Oceananigans` already exports them. The import block shrinks from ~15 lines to 6 + 2 (the two submodule- private names `ERA5HourlySingleLevel` and `PrescribedPrecipitationFlux`). Switch the eight ERA5 single-level downloads from one `Metadata` / `FieldTimeSeries` call per variable to a single `MetadataSet` plus `FieldTimeSeries(forcing_set, land_grid)`, which returns a `NamedTuple` of FTS keyed by variable name. Downstream code reads `era5.temperature[n]`, `era5.surface_pressure[n]`, etc. Net: −94 lines, no behaviour change. Verified end-to-end through the docs `Literate.markdown(...; execute = true)` path (the same one CI uses): 3-day sim runs to completion, animation renders cleanly, output JLD2 + MP4 sizes match the previous version's. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the 3D homogeneous slab example with a 2D vertical-slice LES coupled to a heterogeneous SlabLand and full RRTMGP all-sky radiation, demonstrating the diurnal cycle and wet/dry surface contrast over a single day. A simulation-level callback closes the radiative half of the surface energy balance by adding the net surface radiative flux to the slab's net_energy_flux each step. Rename the ERA5 data-access tutorial to "Exploring ERA5 reanalysis data" (basename exploring_era5_reanalysis_data.jl) to better describe its content; register both as docs examples and add RRTMGP to the docs project. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| | ``β`` | `β` | moisture availability | Moisture availability factor (–) | | ||
| | ``Tᶜ`` | `Tᶜ` | deep climatological temperature | Prescribed deep/climatological target temperature (K) | | ||
| | ``Tᵈ`` | `Tᵈ` | deep temperature state | Prognostic deep-layer soil temperature (K) | | ||
| | ``zʳ`` | `zʳ` | effective root depth | Root-zone scaling of moisture capacity (m) | |
There was a problem hiding this comment.
let's come up with a good symbol for this ("z" as a "Depth" would have to be negative). Maybe just "d"
There was a problem hiding this comment.
"d" is good.
| | ``τˢ`` | `τˢ` | surface-to-deep coupling timescale | Surface/deep temperature relaxation timescale (s) | | ||
| | ``τᵈ`` | `τᵈ` | deep-to-climate coupling timescale | Deep temperature climatological relaxation timescale (s) | |
There was a problem hiding this comment.
we use \tau for surface stresses, so maybe a mathcal T here
There was a problem hiding this comment.
This one is tricky. τ is also used as timescale in Breeze, but I think it is ok to use \mathcal{T} in Breeze.
There was a problem hiding this comment.
true, we may want to change the notation in Breeze too
Move radiation ownership for Breeze-based coupled simulations into
`EarthSystemModel.radiation`. The Breeze `AtmosphereModel` no longer
holds the real `RadiativeTransferModel`; instead it carries a thin
`CoupledRadiation` proxy that aliases the RTM's `flux_divergence`
field, so Breeze's tendency machinery reads radiative heating directly
from the same memory the coupler writes.
The proxy starts as a skeleton (`CoupledRadiation(nothing)`) when
`atmosphere_simulation` is called, contributing zero radiative
tendency and zero extra allocation. Inside `EarthSystemModel(...)`,
the new `materialize_earth_system_radiation!(atmosphere, radiation)`
interface rebuilds the atmosphere with a materialized
`CoupledRadiation(rtm.flux_divergence, rtm)` whenever a
`Breeze.RadiativeTransferModel` is supplied. Breeze's
`update_radiation!` delegates through the proxy back to the real RTM,
honoring its schedule and writing into the shared `flux_divergence`.
Surface radiative coupling moves out of the example: a new
`apply_air_land_radiative_fluxes!` method for
`EarthSystemModel{<:Breeze.RadiativeTransferModel}` reads the RTM's
surface flux fields and adds `-(↑LW + ↓LW + ↓SW)` to the slab's
`net_energy_flux` each step. The example's bespoke
`apply_rrtmgp_to_land!` callback is gone.
Other changes:
- `atmosphere_simulation` returns a `Simulation{<:Breeze.AtmosphereModel}`
for parity with `ocean_simulation`. Drops generic `kw...` splat;
rejects a stray `radiation = Breeze.RadiativeTransferModel`. BreezeExt
dispatches now route through Simulation-typed forwarders that unwrap
`.model`.
- `EarthSystemModel` strips default callbacks from a Simulation-wrapped
atmosphere (matching ocean / sea_ice).
- Add `Accessors` as a direct dependency; `materialize_earth_system_radiation!`
uses `@set` to rebuild the atmosphere with the materialized proxy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
State and parameters in the SlabLand / BucketHydrology stack now read plainly rather than as cryptic single letters: state.W -> state.water_storage (Mˡᵃ, kg m⁻²) field_capacity -> maximum_water_storage (Mˡᵃ⁺, kg m⁻²) critical_wetness -> critical_wetness_ratio (εʷ, dimensionless) Math symbols are defined in `notation.md` and follow the existing system: new base symbol `M` for "layer-integrated mass per area", land component superscript `ˡᵃ` (was `ˡᵈ`), saturation modifier `⁺` (consistent with the existing `pᵛ⁺`). Both the kwarg name on `BucketHydrology` and the Julia identifier in `state` carry the descriptive form; kernel local variables use the math-aligned `M`, `M_max`, `εʷ`. Atmosphere notation gains `qᵛ⁺` (saturation specific humidity), used by the surface-humidity coupling. This is rename-only; no physics changes. Tests, both examples, and the notation reference are updated in step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The bucket cap is enforced by clamping water_storage to [0, Mˡᵃ⁺]; the explicit runoff-rate field had no downstream consumer (no river routing, no land→ocean freshwater coupling). Remove it from the flux accumulators and the step kernel. When downstream coupling needs it, reintroduce as a `runoff(::SlabLand)` diagnostic derived from the imbalance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapse the two-layer Bhumralkar-Deardorff form (prognostic surface T +
prognostic deep T) to a single-layer relaxation: the bulk temperature T
relaxes toward a *prescribed* deep climatology Tᵈᵉᵉᵖ on time scale τ,
∂T/∂t = Q/C + (Tᵈᵉᵉᵖ − T)/τ.
The deep temperature is no longer a state variable — it's an external
target (Number / Field / FieldTimeSeries). Drops the prognostic Tᵈ, the
deep-state initialization kernel, and the second time scale; the two
former time scales (surface_to_deep, deep_to_climate) collapse into one
`deep_time_scale`. prognostic_variables is now (:T,).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SlabLand now exposes its prognostics directly: slab_land.temperature (bulk land T, K) slab_land.water_storage (Mˡᵃ, kg m⁻²) slab_land.moisture_availability (β diagnostic, –) replacing the `state` NamedTuple. The `fluxes` accumulator NamedTuple stays (boundary inputs the coupler fills, not prognostics). All three fields are always allocated; for DryLand/SaturatedSurface the water fields stay zero. Closure interface changes from step!(closure, state, fluxes, surface, grid, Δt[, time]) to step!(closure, land, Δt[, time]) with the closure reading land.temperature / land.water_storage / land.fluxes / land.grid. update_diagnostics!, wetness, and surface_temperature accessors take `land` likewise. This keeps closures testable (step!(land.energy, land, Δt)) and drops the state shim. Because water_storage is always present, the SlabEnergy and ForceRestoreEnergy steps unify to a single kernel using C = Cdry + Cl·water_storage (→ Cdry when dry), removing the no-water/with-water branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: kaiyuan-cheng <74800123+kaiyuan-cheng@users.noreply.github.com>
… wizard The example NaN'd at the first scheduled RRTMGP recompute (t = 10 min): the column was far from radiative equilibrium and the spectral fluxes destabilized over the convecting troposphere. Diagnosed by freezing the radiation (clean for 33+ min) — confirming the recompute, not the land refactor (unit tests + smoke test clean) and not CFL, was the trigger. Restore the stabilizers from Breeze's `radiative_convection` template that this example had dropped: - tropical ozone profile on the BackgroundAtmosphere (stratospheric SW absorption → radiative balance; this was the decisive fix) - stratospheric sponge: Newtonian relaxation of T toward the reference profile above 8 km (built from an explicit ReferenceState/dynamics so the sponge and radiation share thermodynamic constants) - set_to_mean! on the reference state after set! (Float32 accuracy in the tall 15 km column) Also replace the fixed Δt with an every-iteration TimeStepWizard (cfl = 0.7, max_Δt = 6 s) so the step tracks the vertical CFL on the 100 m grid rather than sitting at a hand-picked small value. Validated: full 3-day run completes clean with a strong diurnal cycle — dry edges reach ~331 K at midday while the evaporating wet center stays near ~307 K, relaxing to ~291 K overnight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| # with Γ = 6.5 K km⁻¹. `z_ERA5_eff` is ETOPO box-averaged onto the ERA5 | ||
| # native grid (≈ what ERA5 thinks the surface elevation is in each |
There was a problem hiding this comment.
Worth noting that this procedure is inconsistent with the construction of mean orography in IFS.
From section 11.3.2 in https://www.ecmwf.int/en/elibrary/79697-ifs-documentation-cy41r2-part-iv-physical-processes:
Orography, or geopotential height, is derived from the 30′′data in a few steps:
- The smoothing operator with a Pre-Filter-Scale of 5 km is applied to the 30′′orography.
- The smoothed 30′′data is interpolated to a 5 km grid (reduced Gaussian grid with NGL= 4000). For target resolutions with 4000 grid points or more between the south and north poles (NGL≥4000), the pre-smoothing and interpolation scale of 5 km is reduced to 2 km.
- The 5 km (or 2 km) resolution field is smoothed with the Filter-Scale listed in Table 11.2 dependent on the selected target resolution, e.g. at T1279, the linear grid has NGL= 1280 and a Filter-Scale of 16000 m.
- The resulting field is interpolated to target resolution, e.g. NGL= 1280 for a T1279 linear grid.
- The grid-point orography is transformed to spectral space at target resolution, e.g. T1279.
- The orographic spectrum is tapered by multiplication with 1/[1 + 4{l(l+ 1)}8{lmax(lmax + 1)}−8], where l is the wave number index and lmax is the maximum wave number of the target resolution (e.g. 1279). This is equivalent to smoothing of the grid point field with a 5∇16 operator. The exponent determines the sharpness of the tapering and the factor 5 determines the filtering strength.
- The spectral orography is transformed back to grid point space.
There was a problem hiding this comment.
how would you recommend running a land simulation forced by ERA5, but where the land has a higher resolution than ERA5 thus resolving more topography?
Summary
Adds a composable land-surface component to
EarthSystemModeland 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 axesA
SlabLandis a 2D land model on an Oceananigans(Nx, Ny, Flat)grid composed of three pluggable closures along independent axes:energy :: AbstractEnergyBalance— skin temperaturehydrology :: AbstractHydrology— water budget + moisture availability βsurface :: AbstractSurfaceProperties— albedo / emissivity / roughnessEach closure declares its prognostic state (
prognostic_variables) and the flux/forcing fields it consumes (flux_variables); the container assembles the union as aNamedTuple, so a pure thermal slab carriesstate = (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, …)thenstep!(hydrology, …), refreshes diagnostics viaupdate_diagnostics!inhydrology → surface → energyorder, and fills state halos. Closures read each other's state through the sharedNamedTuplebut 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 capacityW_maxand critical wetnessW_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) andSaturatedSurface(β ≡ 1) — stub hydrologies for tests and idealized runs.Atmosphere–land coupling
AtmosphereLandModelmirrorsAtmosphereOceanModel. A newatmosphere_land_interfaceruns the similarity-theory turbulent-flux pipeline:T,moisture_availability(β), and (optionally)roughness_lengththrough theComponentExchanger;u★, θ★, q★;net_energy_flux = −(𝒬ᵀ + 𝒬ᵛ)(positive into the slab), and the positive/negative parts of the vapor flux intoprecipitation/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 +
SlabLandexample over a homogeneous slab is included (examples/breeze_over_slab_land.jl).Known limitations (follow-ups)
Radiationcomponent.