Add composable SlabLand and atmosphere–land coupling#220
Open
kaiyuan-cheng wants to merge 45 commits into
Open
Add composable SlabLand and atmosphere–land coupling#220kaiyuan-cheng wants to merge 45 commits into
kaiyuan-cheng wants to merge 45 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>
Contributor
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>
glwagner
reviewed
May 11, 2026
glwagner
reviewed
May 11, 2026
glwagner
reviewed
May 11, 2026
glwagner
reviewed
May 11, 2026
glwagner
reviewed
May 11, 2026
glwagner
reviewed
May 11, 2026
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
reviewed
May 11, 2026
glwagner
reviewed
May 11, 2026
Also renames the cached wetness field from `β` to `moisture_availability` for readability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.