Use gt4py staggered dimensions for KHalfDim - #1429
Conversation
Make KHalfDim a real staggered dimension (gtx.flip_staggered(KDim)) instead of
a metadata-only marker, and carry it through common, diffusion and dycore.
Half-level fields are now allocated with nlev+1 levels on KHalfDim rather than
over-allocated on KDim via extend={KDim: 1}, and the KHalfDim -> KDim mapping
in states.factory is removed.
Reads that cross the two grids use half-integer cartesian shifts, which follow
ICON's convention that half level k bounds model level k from above:
div_ic = wgtfac_c * div(KHalfDim + 0.5) + (1 - wgtfac_c) * div(KHalfDim - 0.5)
Programs whose outputs straddle both grids pass a tuple of per-output domains
rather than a single domain dict, keeping the fused field operators intact.
Two helpers needed a per-grid twin because gt4py has no dimension generics:
_cell_2_edge_interpolation and _init_cell_kdim_field_with_zero_wp.
Verified on the embedded backend: dycore stencil tests 75 passed, model/common
606 passed, diffusion 25 passed, and the metrics datatests reproduce the
serialized ICON reference exactly. The metrics -> dycore integration tests and
the gtfn backend are not yet exercised.
Left on KDim for now: pressure_ifc (produced by a KDim scan, consumed only by
muphys/IO), and the tracer_advection, muphys and standalone_driver packages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream 4c858a6 reworks compute_perturbed_quantities_and_interpolation and the surrounding stencils: it merges interpolate_cell_field_to_half_levels_{vp,wp} into one module, renames interpolate_to_surface to extrapolate_quadratically_to_surface, makes exner_at_cells_on_half_levels and perturbed_theta_v_at_cells_on_half_levels program local, and gives the outputs individual domains. Rather than resolve the conflicts line by line, the conflicted files are taken from upstream unchanged and the KHalfDim conversion is re-applied on top. That keeps upstream's restructuring intact and avoids reconciling two rewrites of the same code by hand. Upstream's per-output domains use the same tuple-of-dicts idiom this branch already relies on, so the half-level outputs only needed their dimension switched. Verified on the embedded backend after the merge: dycore stencil tests 76 passed, model/common 606 passed, diffusion 25 passed, and the metrics datatests still reproduce the serialized ICON reference exactly. pre-commit is clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop direct imports of Dimension objects from icon4py.model.common.dimension and reference them through the `dims` namespace, matching upstream #1400. FieldOffsets keep their direct imports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gtfn integration tests exposed three fields with inconsistent vertical grids across the metrics/dycore seam: vn_on_half_levels was still declared EdgeKField in compute_advection_in_horizontal_momentum_equation, while horizontal_advection_of_w_at_edges_on_half_levels and vertical_cfl were still allocated on KDim. z_v_grad_w now spans nlev+1 levels; ICON stores only nlev, so the test compares against the reference extent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
py2fgen takes the array extents from Fortran but the dimension identity from the wrapper annotation, so a KDim-annotated parameter feeding a KHalfDim state field is rejected when the program is called. MetricStateNonHydro.rayleigh_w was still annotated KField while the metrics factory produces and compute_rayleigh_damping_factor consumes KHalfField; the stale annotation made the wrapper look consistent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VerticalGrid is a dataclass and accepts the mislabelled field, but _vct_a reaches _compute_rayleigh_w and _compute_scaling_factor_for_3d_divdamp, which declare fa.KHalfField and reject it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gtfn integration tests rejected three fields still allocated on KDim: nonhydro_buoy_at_cells_on_half_levels, rayleigh_damping_factor and tangential_wind_on_half_levels, the last also carrying a stale EdgeKField annotation on IntermediateFields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Converts p_mflx_contra_v, p_mflx_tracer_v, p_upflux, p_face, vfl_tracer and the z_cfl/z_face buffers, resolving the TODO(dastrm) markers. This also closes the dycore seam: the dycore already produced dynamical_vertical_mass_flux_at_cells_on_half_levels as CellKHalfField into AdvectionPrepAdvState.mass_flx_ic, which was still declared CellKField. Still on KDim: the ppm4gpu stencils and the _k_field index fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rayleigh chain is declared fa.KHalfField in dycore_utils and the implicit solver, so the savepoint readers and the stencil test allocations had to follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dycore_states already declared it CellKHalfField; the test allocations lagged. Also clamp the nonhydro_buoy comparison to the reference extent: ICON's z_th_ddz_exner_c stores only nlev levels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The corrector zeroes prep_adv.dynamical_vertical_mass_flux_at_cells_on_half_levels, now a CellKHalfField, and it already wrote num_levels + 1 levels. Add the missing program wrapper around the existing _init_cell_khalf_field_with_zero_wp and use it. Also clamp the tangential_wind_on_half_levels comparison: ICON's z_vt_ie stores only nlev levels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Separates the recurrence from the staggering, which is what makes this work on every backend: the scan stays on KDim, where its axis matches its output dimension, and a plain field_operator moves the interface pressure onto KHalfDim. The surface level comes from a concat_where on an equality predicate; an open predicate leaves the range unsizable on the embedded backend when the other branch does not span KHalfDim. This also converts pressure_on_cells_half_levels in muphys and driver_io, the pressure_ifc savepoint readers, and DiagnosticState.pressure_ifc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reusing _new_te for pressure_ifc_on_model_levels happened to be safe, since it is rewritten in scatter_to_prognostic before it is read, but a field named after the new temperature is a confusing place to keep a pressure temporary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three ppm4gpu programs write half-level outputs, so their full-level operands (p_cc, p_cellmass_now, z_delta_q, z_a1) are read at KHalfDim +- n.5. The _sum_neighbor_contributions helpers are file-local and only ever called from half-level outputs, so they convert in place. The PiecewiseParabolicMethod index field had to be split: the three ppm4gpu programs index half levels while integrate_tracer_vertically indexes model levels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mass_flx_ic feeds AdvectionPrepAdvState, now a CellKHalfField, so the two branches of _make_prep_adv agree again: the other one forwards the dycore's dynamical_vertical_mass_flux_at_cells_on_half_levels, which has been KHalfDim since the dycore conversion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PPM path zeroes and copies between the full and half vertical grids. Splitting the copy stencils per direction turns each integer offset that crossed grids into a half-integer one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The half-level fixtures were still allocated on KDim, and the numpy references assigned nlev+1 wide results into nlev wide slices. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…level grid Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l grid Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wgtfac_c moved to KHalfDim, so domain[KDim] raises at granule init. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dace cannot lower as_offset applied to a staggered access, so this one stencil keeps the pre-staggering formulation and the caller hands it a zero-copy view of the half-level buffer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The numpy helper was rewritten to take a KHalfDim-wide w, but both composite solver tests still passed it the KDim-truncated slice, so its sweep stopped one half level early and dropped the recurrence term at nlev-2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
When developing, you can test your changes on CSCS CI before merge with the You can pass options to override pipeline variables, for example:
Available options are:
For each option, See The Merging Once your PR is approved and ready for merging, add it to the merge queue. The Optional Tests To run benchmarks you can use:
For more detailed information please look at CI in the EXCLAIM universe. |
Introduces real staggered vertical dimensions using gt4py 1.2.0's
gtx.flip_staggered(ADRnext/0026, GridTools/gt4py#2667), replacing theconvention where half-level fields were
KDimarrays allocated withextend={KDim: 1}.Grid.size[KHalfDim] == num_levels + 1. Static vertical shifts becomehalf-integer cartesian offsets instead of a
FieldOffset.Index convention
Half level
kbounds model levelkfrom above, matching ICON:a(KDim - 0.5)half[k]a(KDim + 0.5)half[k+1]a(KHalfDim + 0.5)full[k]a(KHalfDim - 0.5)full[k-1]The practical rule: an integer
±1offset that used to cross the two gridscollapses into the
±0.5.Scope
All packages that allocate vertical fields:
common,diffusion,dycore,testing,bindings,muphys,microphysics,tracer_advection,standalone_driver.toolsandphysics_driverhave no vertical allocations.Points worth review:
domain=dicts, oneper output field, so a single fused
out=(...)can write fields on differentvertical grids.
pressure_ifcis computed by keeping the scan onKDimand staggering itsresult in a separate plain field operator with
concat_where(KHalfDim == nlev, ...). A scan whose axis differs from itsoutput's vertical dimension is rejected by the embedded backend, and embedded
also cannot take tuple domains together with a scan.
can no longer serve both.
no dimension genericsin gt4py means vertical-agnostic horizontal helpersneed one copy per grid (
_init_cell_khalf_field_with_zero_wpand friends).as_offseton a staggered field: stagger first, then apply the dynamicshift — the offset field must carry the dimension being shifted.
Verification
Against serialized reference data on
gtfn_cpu:test_velocity_advection.pytest_solve_nonhydro.py, driver subsettest_advection.pyPlus the non-datatest suites (
common606,diffusion25,dycorestencil 76,tracer_advection30,muphys58,standalone_driver45), and pre-commit.Worth flagging for reviewers: the dycore stencil suite's 73 xfails are
backend-conditional.
embedded_remap_erroranduses_concat_wherexfail onlyon embedded, which is the backend that suite is normally verified on, so those
fused stencils were never type-checked. Running them on
gtfn_cpushowed theirfixtures still allocated half-level fields on
KDimand their numpy referencesassigned
nlev+1-wide results intonlev-wide slices — several of thoseassignments are broken on
maintoo, as the shared*_numpyhelpers alwaysreturn
nlev+1. Those are fixed here; 14 of the 17 affected files are now greenon
gtfn_cpu.Draft — open items
21 solver-test failuresfixed (136a4fce2). Root cause: the numpy helpersolve_tridiagonal_matrix_for_w_back_substitution_numpywas rewritten to take aKHalfDim-wide
w, but both composite solver tests still handed it theKDim-truncated slice
next_w[:, :n_lev]. Itsnlev = w.shape[1] - 1then cameout one short, so the backward sweep pinned half level nlev-2 instead of nlev-1
and dropped that level's recurrence term; the error propagated upward through
w_new[k+1], decaying by thez_qfactor per level. That is exactly themeasured signature (deviations at k=35..38 only, peak k=37/38, k=39 and k=40
clean, magnitudes in the ratio 1 : 1 : z_q : z_q^2).
test_vertically_implicit_dycore_solver_at_{predictor,corrector}_step.pynowpass 9/9 and 12/12 on gtfn_cpu with the fixtures' usual unseeded random data.
Not yet run: bindings datatests, MPI, diffusion integration,
test_solve_nonhydro.pyin full, and any GPU backend.compute_hydrostatic_correction_termkeeps the pre-staggering formulation andis handed
theta_v_icrelabelled ontoKDimvia a zero-copy view, becausedace cannot lower
as_offsetapplied to a staggered access (standalonereproducer filed separately). It is the one place in the dycore not using
staggering, marked with a TODO.
🤖 Generated with Claude Code