Skip to content

NestedSimulation abstraction + ERA5 regional hindcast example#233

Draft
ewquon wants to merge 24 commits into
mainfrom
eq/era5_breeze_land
Draft

NestedSimulation abstraction + ERA5 regional hindcast example#233
ewquon wants to merge 24 commits into
mainfrom
eq/era5_breeze_land

Conversation

@ewquon
Copy link
Copy Markdown
Collaborator

@ewquon ewquon commented May 13, 2026

Summary

@glwagner @kaiyuan-cheng

Adds examples/era5_breeze.jl — a building block for a regional modeling example that will eventually couple Breeze (compressible solver, in development) to forthcoming SlabLand and SlabOcean components.

Current scope is data ingest only: download ERA5 reanalysis over an SGP-centered LAM bounding box (HI-SCALE 2016-09-10 case day) and interpolate onto a LatitudeLongitudeGrid sized for ~3 km horizontal cells at the domain center latitude. Tᵥ is computed as a derived field using Breeze.ThermodynamicConstants for the Rᵥ/Rd − 1 coefficient.

Out of scope, to be added as the underlying capabilities come online:

  • Breeze model construction
  • dynamical initialization (DFI)
  • open boundary conditions
  • terrain
  • acoustic substepping over terrain
  • land/ocean coupling

Notes

  • Surface pressure is read onto a 3-D grid with Nz=1 rather than a 2-D (Bounded, Bounded, Flat) grid, sidestepping CliMA/Oceananigans.jl#5473 (Flat↔non-Flat interpolate! errors with a cryptic BoundsError; #5474 will convert that to a clear ArgumentError but does not lift the restriction). Mirrors the pattern in examples/ERA5_hourly_data.jl.
  • This example uses a LatitudeLongitudeGrid. A future variant on a projected Cartesian grid would benefit from the set! projection support proposed in Add map projection support to set! for RectilinearGrid targets #232.

Test plan

  • Run end-to-end against the CDS API on a machine with ~/.cdsapirc configured.

@ewquon ewquon marked this pull request as draft May 13, 2026 19:53
Comment thread examples/era5_breeze.jl Outdated
Comment thread examples/era5_breeze.jl Outdated
Comment thread examples/era5_breeze.jl Outdated
using Oceananigans
using Oceananigans.Fields: CenterField, XFaceField, YFaceField
using Breeze
using Breeze.Thermodynamics
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what's this for?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

vapor_gas_constant isn't exported from Breeze.Thermodynamics

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread examples/era5_breeze.jl Outdated
Comment thread examples/era5_breeze.jl Outdated
Comment thread examples/era5_breeze.jl Outdated
@ewquon ewquon force-pushed the eq/era5_breeze_land branch from 9d673f4 to 5463396 Compare May 13, 2026 20:43
Comment thread src/DataWrangling/DataWrangling.jl Outdated
@ewquon
Copy link
Copy Markdown
Collaborator Author

ewquon commented May 14, 2026

Okay, the model state should be ready for dynamic initialization.
era5_breeze_profiles

A couple of important notes:

  1. As noted in ERA5 pressure-level ingest: per-column geopotential and surface-pressure masking #236, we need to better address geopotential height variability and we should discuss the approach to this. I've prototyped per-column vertical interpolation to unblock the ICs work and to have as a reference when developing the data wrangling improvement.
  2. Because we don't have terrain yet, I'm using the height above ground level to interpolate onto the flat-bottomed Breeze grid. (Otherwise, portions of the Breeze domain would be essentially underground and I imagine this would be problematic for the initialization)

@glwagner
Copy link
Copy Markdown
Member

@ewquon should we add terrain? Terrain following should work for the fully compressible formulation (just not substepping)

@ewquon
Copy link
Copy Markdown
Collaborator Author

ewquon commented May 14, 2026

@ewquon should we add terrain? Terrain following should work for the fully compressible formulation (just not substepping)

Hallelujah! I'm happy to add that in. Anything that gets us closer to reality sooner rather than later is a win in my book.

Comment thread examples/era5_breeze.jl
Comment thread docs/make.jl Outdated
@glwagner glwagner added the build all examples add this label to build all the examples in the PR label May 14, 2026
@glwagner
Copy link
Copy Markdown
Member

@ewquon I added the "build all examples" label so that it will build even if the "build always" tag is false.

@glwagner
Copy link
Copy Markdown
Member

@giordano
Copy link
Copy Markdown
Member

giordano commented May 14, 2026

I believe

  failed to register layer: write /usr/local/share/julia/artifacts/75ae42466e7dd21641ff521f63cfb456d09433e2/lib/libcudnn_engines_precompiled.so.9: no space left on device
  Warning: Docker pull failed with exit code 1, back off 8.515 seconds before retry.

simply happens when the runner was reused from a previous run from somewhere else in the organisation, but the new build uses a new Docker image than the previous run, and then it fails to pull the second one because the combination of the two images fills the partition.

In summary, it's an inoffensive, albeit annoying, issue, restarting the job should succeed (unless you rerun in the same situation 😄)

@giordano
Copy link
Copy Markdown
Member

NumericalEarth/Breeze.jl#715 recovers some space on the VMs also in the jobs we run in the Breeze.jl repository (at the moment we do the cleanup only in this repo), hopefully this will make the error less frequent.

@glwagner glwagner changed the title Add ERA5 regional hindcast example NestedSimulation abstraction + ERA5 regional hindcast example May 21, 2026
glwagner and others added 2 commits May 21, 2026 08:56
A parent–child wiring for limited-area runs. `PrescribedAtmosphere` now
returns 3D `(Center, Center, Center)` field-time-series defaults whenever
the grid has `size(grid, 3) > 1`, so it can play the role of a nesting
parent without changes to the existing 2D surface-forcing call sites.

`src/EarthSystemModels/NestedSimulations/` defines:
- a duck-typed parent interface (`parent_clock`, `parent_field`,
  `parent_time_step!`, `parent_update_state!`, `parent_interpolate`)
  with methods for Oceananigans `Simulation`, `FieldTimeSeries`, and
  bare callables; the corresponding `PrescribedAtmosphere` methods
  live next to the type in `prescribed_atmosphere.jl`,
- `NestedSimulation(parent, child::Simulation)` which installs a
  callback that syncs the parent clock to the child after each
  iteration, and forwards `time_step!` / `run!` to the child,
- `parent_boundary_conditions(parent; variables, sides, grid, schemes)`
  which builds a NamedTuple of `FieldBoundaryConditions` populated with
  `OpenBoundaryCondition`s closed over per-side `ParentBoundaryFunction`
  callables (dispatched via `Val(side)` for the right (y,z,t)/(x,z,t)/
  (x,y,t) arity).

Test: `test/test_nested_simulation.jl` exercises both the 2D/3D defaults
and a translating Lamb-Oseen vortex driving an Oceananigans
`NonhydrostaticModel` child.

Also bumps `Breeze` compat to "0.4, 0.5" so the example/regional-hindcast
work can move to Breeze 0.5 once that lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves conflicts in:
- src/NumericalEarth.jl — adopt main's relative-`using .EarthSystemModels`
  style and add `using .EarthSystemModels.NestedSimulations: NestedSimulation,
  parent_boundary_conditions`.
- src/Atmospheres/Atmospheres.jl — adopt main's `using ..EarthSystemModels:
  EarthSystemModels, AbstractPrescribedComponent` style; drop the previous
  `import NumericalEarth.EarthSystemModels.NestedSimulations: parent_*`
  imports along with the rest of the parent-interface scaffolding.
- src/Atmospheres/prescribed_atmosphere.jl — keep main's `Oceananigans.`-
  qualified restore_prognostic_state! signature; drop the parent_clock /
  parent_field / parent_time_step! / parent_update_state! methods on
  PrescribedAtmosphere (no longer needed, see below).

Simplifies NestedSimulations to lean on existing Oceananigans machinery:
- `parent_boundary_conditions(grid; variables, sides, schemes)` now takes
  a NamedTuple of child_field => FieldTimeSeries directly. Per-side closures
  capture the boundary-edge coordinate and call `Oceananigans.Fields.
  interpolate(X, Time(t), fts, …)` from inside an `OpenBoundaryCondition`.
  Standard `ContinuousBoundaryFunction` regularization tags the side — no
  bespoke `ParentBoundaryFunction` struct or `Val(side)` dispatch.
- `NestedSimulation`'s sync callback uses `parent.clock.time` and
  `Oceananigans.TimeSteppers.time_step!(parent, Δt)` directly. The whole
  `parent_interface.jl` (parent_clock, parent_field, parent_time_step!,
  parent_update_state!, parent_interpolate) is removed.

Also renames the regional-hindcast example to "ERA5 downscaling with Breeze
and NestedSimulation" (both the literate header and the docs/make.jl entry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@glwagner
Copy link
Copy Markdown
Member

glwagner commented May 21, 2026

working syntax (updated for the latest API on the branch — child_simulation + NestedSimulation wrap a NestedModel):

using NumericalEarth, Oceananigans, Breeze
using Breeze.AnelasticEquations: AnelasticDynamics
using Breeze.Thermodynamics: ReferenceState

# Lamb-Oseen vortex translating at U in x
const Γ, a, U, x₀, y₀ = 5e3, 200.0, 10.0, 600.0, 1000.0
const ρ̄ = 1.225

function lamb_oseen_uv(x, y, t)
    dx, dy = x - x₀ - U*t, y - y₀
    r² = dx^2 + dy^2
    uθ_over_r =< eps() ? 0.0 :/ (2π * r²)) * (1 - exp(-/ a^2))
    return (U - uθ_over_r * dy, uθ_over_r * dx)
end

# Parent: coarser and strictly larger than the child, so the FTS brackets every
# child sampling node (Interpolated BC + Relaxation-on-FTS both require this).
parent_grid = RectilinearGrid(size = (40, 16, 8),
                              x = (-500, 4500), y = (-500, 2500), z = (-20, 120),
                              topology = (Bounded, Periodic, Bounded))

parent = PrescribedAtmosphere(parent_grid, collect(0.0:5.0:305.0))
set!(parent.velocities.u, (x, y, z, t) -> ρ̄ * lamb_oseen_uv(x, y, t)[1])  # ρ̄·u → Breeze momentum BC
set!(parent.velocities.v, (x, y, z, t) -> ρ̄ * lamb_oseen_uv(x, y, t)[2])

# Child: finer LAM grid
child_grid = RectilinearGrid(size = (128, 32, 4),
                             x = (0, 4000), y = (0, 2000), z = (0, 100),
                             topology = (Bounded, Periodic, Bounded))

# Sponge mask: ramps from 0 in the interior to 1 in the outer 15% of x.
const Lx, x_sponge = 4000.0, 0.85 * 4000.0
sponge_mask(x, y, z) = x < x_sponge ? 0.0 : ((x - x_sponge) / (Lx - x_sponge))^2

# child_simulation builds the Breeze child model, wires Open BCs from the parent
# on the requested sides, and adds Relaxation-on-FTS interior nudging.
# It returns the constructed AtmosphereModel.
ref   = ReferenceState(child_grid; surface_pressure = 101325.0, potential_temperature = 300.0)
child = child_simulation(AtmosphereModel, child_grid, parent;
                    sides           = (:west, :east),
                    relaxation_rate = 1 / 60,        # 60-second sponge timescale
                    relaxation_mask = sponge_mask,
                    dynamics    = AnelasticDynamics(ref),
                    formulation = :LiquidIcePotentialTemperature,
                    advection   = WENO(order = 5))

set!(child; θ  = 300.0,
            ρu = (x, y, z) -> ρ̄ * lamb_oseen_uv(x, y, 0)[1],
            ρv = (x, y, z) -> ρ̄ * lamb_oseen_uv(x, y, 0)[2],
            ρw = 0.0)

# NestedSimulation(parent, child; sim_kwargs...) is a convenience for
# `Simulation(NestedModel(parent, child); sim_kwargs...)`. The NestedModel's
# `time_step!` steps the child, then ticks the parent's clock to match.
nested = NestedSimulation(parent, child; Δt = 0.2, stop_time = 300.0)
run!(nested)

What's in play:

  • child_simulation(modeltype, grid, parent; …) builds the child model and wires up its BCs and forcings from the parent. Returns the constructed AbstractModel. Forwards model_kwargs to the model constructor.
  • Variable mapping is chosen by dispatch on default_parent_variables(modeltype, parent). Oceananigans.NonhydrostaticModel(u, v) ← parent.velocities.(u, v) (core). Breeze.AtmosphereModel(ρu, ρv) ← parent.velocities.(u, v) (in NumericalEarthBreezeExt, so Breeze stays a weakdep).
  • BCs go through an internal Interpolated(fts) wrapper that lets OpenBoundaryCondition consume an FTS directly. Standard Oceananigans regularization tags it with side/dim/location; our getbc does trilinear-in-space + linear-in-time interpolation against the parent grid.
  • relaxation_rate / relaxation_mask route through Oceananigans' native FTS-Relaxation path (Support FieldTimeSeries targets in Relaxation CliMA/Oceananigans.jl#5575) for interior nudging.
  • NestedModel(parent, child::AbstractModel) <: AbstractModel pairs the two and overrides time_step! to step the child, then advance the parent. NestedSimulation(parent, child; sim_kwargs...) is a one-liner for Simulation(NestedModel(parent, child); sim_kwargs...).

Needs Breeze on eq/atmosphere-model-open-bc-velocity (PR NumericalEarth/Breeze.jl#722) and Oceananigans main (post-0.108.0) for FTS-Relaxation.

glwagner and others added 4 commits May 21, 2026 14:45
`child_simulation(modeltype, grid, parent; …)` builds a `NestedSimulation`
in a single call: it wires `parent_boundary_conditions` for the open sides,
optionally `parent_forcings` for interior relaxation, constructs the child
model and `Simulation`, and installs the parent-sync callback.

The variable mapping (which parent FieldTimeSeries drives which child field)
is chosen by dispatch on `default_parent_variables(modeltype, parent)`:
- Oceananigans `NonhydrostaticModel` → `(u, v) ← parent.velocities.(u, v)`.
- Breeze `AtmosphereModel` → `(ρu, ρv) ← parent.velocities.(u, v)` (assumes
  the parent stores ρ̄·u in the velocity slots). Lives in
  `NumericalEarthBreezeExt` so Breeze stays a weakdep.

`parent_forcings(; variables, rate, mask)` returns a NamedTuple of
`Oceananigans.Forcings.Relaxation`, one per variable, whose target is a
closure reusing the same `Oceananigans.Fields.interpolate(X, Time(t), fts, …)`
path as the BC closures. `rate` and `mask` accept scalars, callables, fields,
or per-variable NamedTuples; the existing Oceananigans mask types
(`GaussianMask`, etc.) work straight through.

`child_simulation` exposes this via `relaxation_rate` / `relaxation_mask`
kwargs and merges with any user-supplied `forcing` already in `model_kwargs`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Oceananigans main natively supports `Relaxation(rate, mask, target=fts)`:
`materialize_forcing` wraps the FTS in a `FieldTimeSeriesTarget` that
handles space/time interpolation and GPU adaptation. Dropping the closure
wrapper we used before — that path silently extrapolates near boundaries,
whereas the FTS-direct path runs `validate_fts_target_extent` to ensure the
parent FTS strictly brackets every child sampling position.

`parent_boundary_conditions` still uses its per-side closures since the BC
machinery doesn't yet have an analogous native FTS path.

Compat note: needs the Oceananigans release that bundles FTS-Relaxation
support (currently main, post-0.108.0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets `OpenBoundaryCondition(fts)` work directly, instead of requiring user
code to wrap the FTS in per-side closures. The pattern mirrors what
Oceananigans `Relaxation` does for FTS targets:

- `InterpolatedFTSBoundary{Dim, SideType, LX, LY, LZ, FTS}` carries the
  boundary-normal axis, side, and the child field's location at the
  boundary face as type parameters.
- `Oceananigans.BoundaryConditions.regularize_boundary_condition(::FlavorOfFTS, …)`
  builds the side-tagged condition during normal Oceananigans regularization,
  and runs the same strict bracketing check Oceananigans uses for
  `Relaxation`-on-FTS (FTS node extents must contain every child sampling
  position).
- `getbc` methods for dims 1/2/3 compute `node(i, j, k, grid, LX(), LY(), LZ())`
  at the boundary face and call `Oceananigans.Fields.interpolate(X, Time(t), fts, …)`.

`parent_boundary_conditions` collapses to one line per side — no closures,
no `Val(side)` dispatch, no edge-coord precomputation in user code. The
implementation lives in NestedSimulations (mild type piracy on
`regularize_boundary_condition(::FieldTimeSeries, …)`) until an analogous
path lands upstream in Oceananigans, after which this file becomes a
deprecation shim.

Test updated to use a parent grid that strictly brackets the child (required
by the same validation used for Relaxation). 13/13 still pass.

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

The previous version added a `regularize_boundary_condition(::FieldTimeSeries, …)`
method — type piracy on an Oceananigans-owned function with an
Oceananigans-owned argument type. Replace with our own wrapper type:

    OpenBoundaryCondition(Interpolated(fts))

`Interpolated{Dim, Side, LX, LY, LZ, FTS}` carries the side / dimension /
field-location tags as type parameters. The user-facing constructor leaves
them all `Nothing`; Oceananigans' regularization runs through our
`regularize_boundary_condition(::Interpolated{Nothing}, …)` method and
fills in the type parameters. Same bracketing validation, same `getbc`
machinery, no piracy.

`Interpolated` is intentionally not exported — it's an internal wrapper
used by `parent_boundary_conditions`. The public surface stays
`NestedSimulation`, `parent_boundary_conditions`, `parent_forcings`,
`child_simulation`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
glwagner and others added 4 commits May 21, 2026 17:58
…s a wrapper

Parent-sync moves out of a callback on the child `Simulation` and into the
integrator itself. The new layering:

- `NestedModel(parent, child::AbstractModel) <: AbstractModel` pairs the two
  and overrides `time_step!` to step the child, then advance the parent to
  match the new child clock. Property access (`clock`, `grid`, `velocities`,
  …) forwards to the child; `fields`, `prognostic_fields`, `architecture`,
  `iteration`, `update_state!`, and the checkpointing pair forward via
  explicit methods. The model now satisfies the `AbstractModel` protocol —
  plain `Simulation(NestedModel(…))` just works.

- `NestedSimulation(parent, child_model::AbstractModel; sim_kwargs...)` is
  a one-line convenience: `Simulation(NestedModel(parent, child_model); …)`.
  No callback installation, no hidden integrator state.

- `child_simulation` → `child_model`: renamed (and the file moved), returns
  the constructed child model instead of a `Simulation`. The caller follows
  up with `NestedSimulation(parent, child; Δt, stop_time, …)`. Matching
  rename on the Breeze ext file (`breeze_child_simulation.jl` →
  `breeze_child_model.jl`).

`NumericalEarth.jl` exports both `NestedModel` and `NestedSimulation`. Test
collapses to the new two-call form; 13/13 still pass.

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

Keeps the name `child_simulation` (matching `ocean_simulation` /
`atmosphere_simulation` convention) while still returning an `AbstractModel`
— the Breeze `atmosphere_simulation` helper returns a model, not a
`Simulation`, and we follow that convention so the call site composes:

    child  = child_simulation(modeltype, grid, parent; …)
    nested = NestedSimulation(parent, child; Δt, stop_time, …)

The underlying model constructor is now an extension point:
`_build_child_model(modeltype, grid; kwargs...)` defaults to
`modeltype(grid; kwargs...)`; the Breeze ext overrides it for
`AtmosphereModel` to dispatch through `atmosphere_simulation`. That way the
Breeze call picks up Breeze's default advection / microphysics when the user
doesn't override them, rather than reimplementing those defaults here.

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

build all examples add this label to build all the examples in the PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants