Per-column z ingest for ERA5 pressure-level data#241
Merged
Conversation
Adds `PressureLevelVerticalDiscretization` (new `src/Grids` submodule), a 3-D vertical coordinate backed by raw geopotential `Φ(λ,φ,p)` (m²/s²) and a `gravitational_acceleration` scalar. `rnode(i,j,k,grid)` reads `Φ[i,j,k] / g`, and `_fractional_indices` is specialized for the `PressureLevelGrid` alias to bisect each column's actual heights instead of the 1-D `znodes(grid, loc)` vector that the default kernel assumes. `ERA5HourlyPressureLevels` / `ERA5MonthlyPressureLevels` gain a tri-state `z_mode` keyword (`:per_column` default, `:mean`, `:standard_atmosphere`), replacing the boolean `mean_geopotential_height` with a deprecation shim. In `:per_column` mode, `z_interfaces` returns the new discretization built from the instantaneous Φ and clipped at the local surface geopotential (`ERA5HourlySingleLevel` / `ERA5MonthlySingleLevel` paired via dispatch on the pressure-level dataset type). `examples/ERA5_hourly_data.jl` gains a final §4 section that animates ERA5 specific humidity at z = 800 m over the RICO bbox, downscaled 4× from the 0.25° native grid — the entire ingest is one `set!(field_2d, metadatum)` per frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
`z_mode` is removed entirely. The only honest mode is per-column, and `ERA5HourlyPressureLevels`/`ERA5MonthlyPressureLevels` always produce it when `z = nothing` (the default). Users who explicitly want a static profile pass `z = standard_atmosphere_z_interfaces(pressure_levels)` (or any other vector / discretization) directly. `PressureLevelVerticalDiscretization` no longer carries `cᵃᵃᶠ`/`cᵃᵃᶜ`/ `Δᵃᵃᶠ`/`Δᵃᵃᶜ` placeholders — only `gravitational_acceleration` and `geopotential`. `generate_coordinate` computes `grid.Lz` from `extrema(geopotential) / g` so it reports the actual physical extent (~47 km for ERA5's full level set) instead of a meaningless `Nz`. `znodes`/`rnodes` (the 1-D plural forms) error explicitly on a `PressureLevelGrid`: there is no representative 1-D z. Per-cell `znode(i, j, k, grid)` still works and is what `_fractional_indices` and downstream interpolation use. Also pulls in the `:geopotential` / `:geopotential_height` entries on `ERA5HourlySingleLevel`'s variable-name maps (previously only on the era5_breeze_land branch) so that the surface-clip path in `per_column_geopotential_discretization` actually resolves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match Oceananigans' existing `rnodes(::AUG, ...)` signatures one-to-one with strictly-more-specific dispatch on `PressureLevelGrid`. Previously the generic AUG fallback's vararg + my vararg fallback created ambiguity, and `znodes(grid, Center())` failed with `MethodError`; now it throws a clear `ArgumentError` pointing users at `znode(i, j, k, grid)`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eanup
- Parameterize `ERA5HourlyPressureLevels{Z}` and `ERA5MonthlyPressureLevels{Z}`
by the `z` field's type instead of carrying `::Any`.
- `clip_subsurface!(::Field, Φ_sfc)` now launches a KA kernel via `launch!`
so it works on GPU fields. CPU loop kept for the TSI path (not exercised).
- `rnodes(grid::PressureLevelGrid, ℓx, ℓy, ℓz)` returns a
`KernelFunctionOperation` wrapping per-cell `rnode`, so callers can
materialize the 3-D z-field via `Field(rnodes(...))`. The 1-loc form
still throws an ArgumentError (no 1-D answer exists).
- Reuse Oceananigans' `index_binary_search` via a tiny `_ColumnView`
wrapper instead of reimplementing the bisection.
- Fix docstring placement (was attached to the @kernel definition instead
of the public `clip_subsurface!`). Drop stale references to removed
`z_mode` and `column_index_binary_search`.
- Remove alignment-style padding around `=` and `::`. Reorder local
helpers (`_ColumnView`, `_znode_op`) to precede their use.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
|
here's a movie that can be created after this PR, atmos state interpolated to a constant z-level helene_animation_demo.mp4 |
The `mean_geopotential_height::Bool` field was removed when `z_mode` was dropped in favor of `z = nothing` triggering the per-column path. Update the constructor-sorting test accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Constrain the 3-tuple `_fractional_indices` method to `NTuple{3, Any}` so it
no longer matches the 1-tuple (z,) call that Oceananigans uses for
Flat-Flat-Bounded grids (a Column-region source). Add a 1-tuple method that
bisects the single (1,1) column directly.
Fixes docs build, which evaluates §3 of the ERA5 tutorial — the RICO column
path that loads `:specific_cloud_liquid_water_content` over a `Column` region.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A `znodes(field)` call on a Column-region field (Field{Nothing, Nothing, Center}
on a Flat-Flat-Bounded grid) routed to the 3-loc `rnodes` override and returned
a `KernelFunctionOperation`, which Makie's heatmap recipe rejected — fails
docs build for §3 of the ERA5 tutorial.
Add an explicit `rnodes(grid, ::Nothing, ::Nothing, ℓz)` that materializes the
single (1, 1) column's heights as a `Vector{Float64}`. The 3-D form (non-
Nothing ℓx/ℓy) still returns a lazy `KernelFunctionOperation`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous KernelFunctionOperation return from `rnodes(grid, ℓx, ℓy, ℓz)`
broke any consumer expecting a 1-D plot axis or `length`-able vector —
including `znodes(T_series[1])` in §3 of the ERA5 tutorial, which then hits
a DimensionMismatch when allocating `zeros(Nz, Nt)` (since `length` on the
3-D KFO is Nx·Ny·Nz, not Nz).
Switch every `rnodes(grid::PressureLevelGrid, ...)` form to return the
column-mean z profile as a plain `Vector{Float64}` of length `Nz`. This
matches what the old `:mean` ingest mode produced and what plot recipes
expect. Per-cell access is unchanged: `znode(i, j, k, grid)` still reads
`geopotential[i, j, k] / g`. Users who want a lazy 3-D z-field can build
one explicitly from `_znode_op` (no longer in the public surface — drop
the import since nothing in the package needs it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…umericalEarth/NumericalEarth.jl into eq/per-column-z-discretization
navidcy
reviewed
May 16, 2026
glwagner
added a commit
that referenced
this pull request
May 16, 2026
Addresses review feedback on PR #241: prefer `view` semantics over materializing into a fresh `Array`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses review feedback on PR #241: prefer `view` semantics over materializing into a fresh `Array`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8e94880 to
7d44c3f
Compare
- Drop `pressure_level_heights` helper: `znodes(field)` already returns
the column-mean `Vector{Float64}` for `PressureLevelGrid` (commit
53f1786). `Field(znodes(field))` doesn't exist as a constructor.
- §1: pass the wind/Stokes `AbstractOperation`s directly to `heatmap!`;
the Makie ext converts and supplies node coordinates.
- §4: hold the `CenterField` itself in the `Observable` and let the ext
handle the update path, instead of unpacking `interior(q2d)[:, :, 1]`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A field on a PressureLevelGrid has per-cell heights — there's no single
1-D z axis at the Field level. Override `znodes(::Field)` to return a
materialized 3-D `Field` of per-cell z (wrapping a `KernelFunctionOperation`
over `rnode`), keeping the field's own location.
The grid-level `znodes(grid, ℓ...)` / `rnodes(grid, ℓ...)` still return
the column-mean `Vector{Float64}` so plot recipes, `Lz`, and length
consumers that only have the grid in hand continue to work.
Example: collapse to a 1-D axis via `vec(znodes(field))` for column
(1×1×Nz) fields, or `vec(mean(znodes(field), dims=(1, 2)))` for
horizontally extended fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same numerical result for the (1×1×Nz) Column case (it's a no-op mean), but matches the profiles-plot pattern and states intent clearly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`znodes(grid, Center(), Center(), Center())` already returns the
column-mean `Vector{Float64}` we want for the plot axis, so we don't
need to round-trip through `vec(mean(znodes(field), dims=(1, 2)))`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`znodes(grid, nothing, nothing, Center())` is the semantically honest way to ask for the horizontally-averaged z profile — `Nothing` slots say "no horizontal dimension," which is what a column-mean axis is. The existing `rnodes(::PressureLevelGrid, ℓz::Center)` 1-arg override already handles this routing (Oceananigans' default 3-arg `rnodes` forwards to the 1-arg form). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ewquon
reviewed
May 19, 2026
ewquon
reviewed
May 19, 2026
Per the convention reaffirmed on PR #241: the leading `_` is reserved for KernelAbstractions `@kernel` functions and `func`/`_func` layered interfaces. Everything else is module-private by virtue of not being `export`ed. Renames in this PR's new code: ext/NumericalEarthCDSAPIExt.jl _CDS_MAX_FIELDS_PER_REQUEST → CDS_MAX_FIELDS_PER_REQUEST _group_by_calendar_month → group_by_calendar_month _max_dts_per_cds_request → max_dts_per_cds_request _batch_datetimes_for_cds → batch_datetimes_for_cds src/Grids/pressure_level_vertical_discretization.jl _geopotential_data_for_extrema → geopotential_data_for_extrema _mean_height_profile → mean_height_profile _znode_op → znode_op _znodes_for_plg → znodes_for_plg _ColumnView → ColumnView test/test_pressure_level_grid.jl _make_plvd → make_plvd _make_plg → make_plg Kept: _clip_subsurface_kernel! (a `@kernel` function) _fractional_indices, fractional_x_index, … (Oceananigans' names — imported, not introduced by this PR) All 58 PressureLevelVerticalDiscretization tests and 320 CDSAPIExt tests still pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ewquon
reviewed
May 19, 2026
Force-push of MetadataSet export fix didn't auto-trigger CI.
Co-authored-by: Eliot Quon <eliot@aeolus.earth>
Neither type is referenced unqualified from outside the `Grids` submodule — `src/DataWrangling/ERA5/…` already imports them via `using NumericalEarth.Grids: PressureLevelVerticalDiscretization`, and `PressureLevelGrid` only appears in doc cross-refs and a comment in the example. Resolves ewquon's question on PR #241. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ewquon
reviewed
May 19, 2026
ewquon
reviewed
May 19, 2026
ewquon
reviewed
May 19, 2026
ewquon
reviewed
May 19, 2026
`parent(fts)` returns a 4-D OffsetArray that includes halo cells. For default FieldBoundaryConditions those halos are zero, which collapsed both `extrema` (driving `Lz`) and the per-level `mean` in `mean_height_profile` toward zero. ewquon's repro on PR #241: znodes(grid, Center()) → [0.0, 0.0, 0.0, 480.0] expected → [3000.0, 6000.0, 9000.0, 12000.0] Use `interior(fts)` instead, which already excludes halos on every spatial dim while preserving the trailing time axis — matching the `Field` overload one line up. Adds a regression test asserting the time-mean column-mean and `Lz` on a TSI-backed PLVD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Define `Base.show(io, grid::LatitudeLongitudeGrid{…<:PLVD},
withsummary=true)`. The default `LatitudeLongitudeGrid` show reads
`grid.z.cᵃᵃᶠ`, which `PressureLevelVerticalDiscretization` doesn't
carry — typing the grid name at the REPL raised
`FieldError: PressureLevelVerticalDiscretization has no field cᵃᵃᶠ`.
The override prints the horizontal axes, size/halo/Lz, and delegates
the z line to PLVD's own `show`. Targeting `LLG{…<:PLVD}` (concrete
wrapper + constrained `z` parameter) is strictly more specific than
either the LLG default or `show(::PressureLevelGrid)`, so dispatch
is unambiguous. Resolves ewquon on PR #241.
- Drop the unused `locs` arg from `column_fractional_z_index`; callers
in both `_fractional_indices` overloads stop passing it. Resolves
ewquon on PR #241.
Regression test asserts that both `sprint(show, grid)` and
`sprint(show, MIME"text/plain"(), grid)` complete without `FieldError`
and include the PLVD description.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ewquon
approved these changes
May 19, 2026
`test_quality_assurance.jl` (CPU CI on Julia 1.12.5) flagged two issues
in `NumericalEarth.Grids` introduced by this PR:
ExplicitImportsFromNonOwnerException:
`instantiated_location` has owner `Oceananigans` but it was imported
from `Oceananigans.Fields`.
StaleImportsException:
`AbstractField` is imported but unused (the type was used in an
earlier draft of `_znodes_for_plg`; switched to two concrete
dispatch methods on `Field` / `FieldTimeSeries` in 8664a5d and
never cleaned up).
Move `instantiated_location` to the top-level `Oceananigans` import and
drop the unused `AbstractField`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ext/NumericalEarthCDSAPIExt.jl` does `using Dates: Dates` (module-only
import per the ExplicitImports convention), so `DateTime` must be
written as `Dates.DateTime`. The docs build picked up the unqualified
reference in `batch_datetimes_for_cds`:
ERROR: LoadError: LoadError: UndefVarError: `DateTime` not defined
in `NumericalEarthCDSAPIExt`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
|
merging this because the CI errors are unrelated. |
glwagner
added a commit
that referenced
this pull request
May 20, 2026
Resolves conflicts from PR #241 (per-column z ingest) landing on main: - src/DataWrangling/ERA5/ERA5.jl: take main's PressureLevelVerticalDiscretization import, keep our download_dataset → download rename. - src/DataWrangling/ERA5/ERA5_pressure_levels.jl: take main's wholesale rewrite (per_column_geopotential_discretization, parametric {Z} types), adapt the two download_dataset calls to use the renamed download. - examples/ERA5_hourly_data.jl: keep our lower-troposphere bullet, use download instead of download_dataset. 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
PressureLevelVerticalDiscretization+PressureLevelGrid(newsrc/Gridssubmodule). Stores raw geopotentialΦ(λ,φ,p)(m²/s²) andgravitational_acceleration;rnode(i,j,k,grid)readsΦ[i,j,k] / g._fractional_indicesis specialized soOceananigans.interpolate!bisects each column's actual heights inside the kernel, instead of the 1-Dznodes(grid, loc)vector that the default assumes.z_interfaces(::ERA5PressureMetadata)builds the discretization from the instantaneous Φ and clips at the local surface geopotential viaclip_subsurface!. No new dataset keyword — oldz_mode/mean_geopotential_heightplumbing is gone.znodesdispatch onAbstractField. Returns the column-meanVector{Float64}when the field is horizontally absent (Flatx/y topology orNothinghorizontal locations frommean(...; dims=(1,2))); otherwise a 3-D per-cellField. Same logic applies toFieldTimeSeries, somean(znodes(T_series), dims=(1,2))yields aField{Nothing, Nothing, Center}as expected.(year, month)(using CDS's Cartesianyear/month/day/timesemantics) and further cap each batch at 5000 fields per request to avoid HTTP 403 cost-limit errors on pressure-level multi-var pulls.examples/ERA5_hourly_data.jl. Adds a §4 that animates ERA5 specific humidity downscaled 4× from the 0.25° native grid onto a fixed-altitude target, illustrating the per-column-z interpolation end-to-end. Plotting routes through the Oceananigans Makie ext (Fields and AbstractOperations as plot args;Observable{<:Field}for animation). The §4 frame loop reuses the §3cq_seriesFieldTimeSeriesviainterpolate!, so no per-frame metadata lookup or grid reconstruction.rico_regionfrom 1.5°×1.5° to 1°×1°. Cold-cache local repro: 53 min → 30 min.Closes #236.
Why
set!(field, era5_pressure_level_metadatum)previously placed the source ERA5 field on aLatitudeLongitudeGridwhose z-axis was a single 1-D vector of⟨Φ⟩/gaveraged over space and time. Over real terrain (sub-surface ERA5 fill leaks through that flat profile) and under synoptic Φ-surface displacement (~40 m for 5 hPa SLP gradients — comparable to LAM near-surface Δz), that profile is wrong by O(50–500 m) in cell placement. See #236 for details and the design write-up.Design highlights
rnodeand_fractional_indices; advection, halo, and model-side kernels never touch it because pressure-level grids aren't used as model grids.clip_subsurface!rewritesΦ[i,j,k] ← max(Φ[i,j,k], Φ_sfc[i,j])in place and refills halos. Columns are guaranteed monotone in k, socolumn_fractional_z_indexneeds no branching and the kernel stays clean._matching_single_level_dataset(::ERA5HourlyPressureLevels) = ERA5HourlySingleLevel()and the monthly counterpart, so the surface-clip source matches the cadence of the data being clipped.znodes(::Field)/znodes(::FieldTimeSeries)route via_znodes_for_plg. Two concrete-type methods (one onField{…<:PressureLevelGrid}, one onFieldTimeSeries{…<:PressureLevelGrid}) — each more specific than Oceananigans' ownznodesdefaults on their respective wrappers, so dispatch is unambiguous._batch_datetimes_for_cds. Groups by(year, month)first (preserving CDS's Cartesian product), then chunks each month into ≤5000 / (num_vars × num_levels)datetimes. Recovers most of the daily→monthly speedup for small payloads (§2/§3a: 2.5–3.3×) while staying safely under CDS's pressure-level cost limit.What this defers
TimeSeriesInterpolation-backedΦfor forecast-time use. The discretization is already structurally compatible (itsgeopotentialfield accepts a TSI), butERA5PrescribedAtmosphereand thegeopotentialfield onPrescribedAtmospherearen't here — separate PR._interpolate.column_fractional_z_indexbisects a single representative column — the cell at the integer indicesclamp(unsafe_trunc(Int, ii), 1, Nx),clamp(unsafe_trunc(Int, jj), 1, Ny). The bilinear blend inOceananigans.interpolate!then weights values at the neighbouring(i+1, j),(i, j+1),(i+1, j+1)columns with the same(k_low, k_high), even though those columns have slightly different z values at the same k. Near sharp orographic gradients this biases the result. Tractable as a kernel-level follow-up.FieldTimeSeriessupport inOceananigansMakieExt—heatmap!(ax, fts)would let the §3 Hovmöller drop its manual(Nt × Nz)matrix assembly. Tracked upstream as CliMA/Oceananigans.jl#5599.Test plan
test/test_pressure_level_grid.jl, 58 tests):PressureLevelVerticalDiscretizationconstructor,generate_coordinatedim/axis guards,clip_subsurface!behavior on aField-backed Φ, grid-levelznodes/rnodesagreement,znodes(::Field)per-cell vs column-mean dispatch on all four cases (horizontally resolved,Flat-Flattopology,Nothing-Nothinglocation,FieldTimeSeriesparity).test/test_cds_downloading.jl):_group_by_calendar_month,_batch_datetimes_for_cds(single-level monthly fits in one batch; pressure-level many-vars splits within a month;_max_dts_per_cds_requestarithmetic; chronological ordering under shuffled input).Adaptimport cleanup insrc/Grids/pressure_level_vertical_discretization.jl.examples/era5_breeze.jland confirm field ranges match the previous two-stage workaround to within rectification bias — out of scope for this PR.🤖 Generated with Claude Code