Skip to content

Merge VelocityAdvection into two programs with per-output domains - #1460

Draft
havogt wants to merge 25 commits into
mainfrom
program-audit
Draft

Merge VelocityAdvection into two programs with per-output domains#1460
havogt wants to merge 25 commits into
mainfrom
program-audit

Conversation

@havogt

@havogt havogt commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Part of the Audit ICON4Py programs with new GT4Py features project. First unit of work: VelocityAdvection in its final shape.

What this is for

A field_operator call is the granularity at which the GT4Py optimization pipeline works. When two calls are separate, the values passing between them are program-level fields the pipeline must treat as materialized and visible. Merging them moves that work inside one unit of optimization, so the pipeline becomes free to fuse it, keep it in registers, or still materialize it.

This is not a claim about kernel count or memory — both remain the pipeline's decision and neither is measured here. Performance will be measured separately, on santis; the pitch expects the new structure may temporarily hurt performance until optimization passes adapt.

What changed

VelocityAdvection issued 5 program calls across its two run methods; it now issues 2, each a single field_operator call with a per-output domain.

  • run_predictor_step: 3 programs → compute_velocity_advection_in_predictor_step (9 outputs)
  • run_corrector_step: 2 programs → compute_velocity_advection_in_corrector_step (3 outputs)

_allocate_local_fields goes from 3 fields to 1. horizontal_advection_of_w_at_edges_on_half_levels (ICON z_v_grad_w) and contravariant_corrected_w_at_cells_on_model_levels (ICON z_w_con_c_full) are no longer program fields; GT4Py derives their extents from the neighbour reads of their consumers. The predictor's vertical-momentum operator now computes the horizontal advection of w inline, which is how the corrector already did it.

Both host max_vertical_cfl reductions moved after the fused call — safe because apply_extra_diffusion_on_vn = True is a hardcoded literal at both sites and nothing between the calls consumed the reduction.

Three outputs got their own narrowed domain, each derived from mo_velocity_advection.f90 rather than from the previous icon4py bounds:

Output Domain Fortran
contravariant_correction_at_edges_on_model_levels KDim: (nflatlev, nlev) DO jk = nflatlev_jg, nlev
vertical_wind_advective_tendency (both programs) KHalfDim: (1, nlev) DO jk = 2, nlev
vn_on_half_levels KHalfDim: (0, nlev+1) absorbs the separate top-level extrapolation

Every other per-output domain is the one its predecessor program used, checked against the Fortran and against the neighbour reach of each consumer. The wider-than-necessary cell domains were deliberately not narrowed to match their consumers: that slack is what keeps the C2E/E2C reads in bounds.

Aliasing (fields both read and written in the same statement, the gt4py#2817 hazard) drops from 3 statements / 5 fields to 1 statement / 2 fields. Removing the two concat_where(1 <= KHalfDim, …) guards meant the operator stops reading the incoming tendency altogether, which cleared the corrector's aliasing entirely.

The four superseded programs are deleted along with their stencil tests; every field_operator they used survives and is exercised through the merged programs.

Verification

Suite Backend Result
test_compute_velocity_advection.py gtfn_cpu 12 passed
test_compute_velocity_advection.py dace_cpu 11 passed, 1 xfailed
test_velocity_advection.py --datatest-only gtfn_cpu / dace_cpu 10 passed each
test_solve_nonhydro.py (4 dycore cases) gtfn_cpu / dace_cpu 8 passed each

No --skip-stenciltest-verification anywhere. The embedded backend is not evidence here — these tests xfail on it, and an xfailing stencil test never evaluates its numpy reference, so no claim above rests on it.

Savepoint comparisons lost, both inherent to the demotions: z_v_grad_w and z_w_con_c_full. To compensate, z_vt_ie, z_kin_hor_e, z_w_concorr_me and a cfl_clipping mask comparison were added to the component tests, where none of them existed before.

Known issues

One xfail, strict=True. One dace_cpu variant hits assert read_right or read_left in splitting_tools.py:515, reached from SplitMemlet.can_be_applied: both flags come from (a < b) == True on SymPy relationals, which is undecidable — hence False both ways — when horizontal bounds are symbolic while vertical ones are static. The merge exposes this rather than causing it; the three predecessor programs pass the same variants on 4a064f23c. The model is unaffected: setup_program binds horizontal and vertical bounds together, so the fully-static shape is what ever gets compiled, and it passes on dace, as do both dace datatest suites. A gt4py-only reproducer is being prepared separately.

A pre-existing icon4py/ICON divergence, now documented in a test. When skip_compute_predictor_vertical_advection=True, icon4py leaves z_vt_ie untouched while ICON sets half level 0 unconditionally (mo_velocity_advection.f90:300, outside the IF (.NOT. lvn_only)). 29020 edges differ, level 0 only, on MCH_CH_R04B09. ICON itself notes that level is unused. Unchanged by this PR — the new assertion compares from level 1 and names the divergence.

scripts/python/compare_icon4py_openacc.py loses four stale entries: its model is one icon4py program per OpenACC kernel, and each merged program now aggregates several. Restoring that comparison needs a decision from whoever owns the perf tooling.

Module consolidation and dead-code removal

The 15 field_operators with no live consumer outside these two programs moved into compute_velocity_advection.py (now 1300 lines, 17 operators + 2 programs). compute_advection_in_{vertical,horizontal}_momentum_equation.py are deleted; compute_diagnostics_from_normal_wind.py is down to 36 lines, holding the two operators compute_horizontal_velocity_quantities.py still imports. This is pure code motion — every moved operator's AST is byte-identical before and after, and the generated C++ was checked to be byte-identical by diff against a separate clean build cache.

Separately, five superseded modules are deleted with their stencil tests: the old _mo_velocity_advection_stencil_05/14/15 ports (compute_horizontal_kinetic_energy, interpolate_vn_to_half_levels_and_compute_kinetic_energy_on_edges, interpolate_vn_and_vt_to_ie_and_compute_ekin_on_edges, compute_maximum_cfl_and_clip_contravariant_vertical_velocity, interpolate_contravariant_vertical_velocity_to_full_levels). Nothing in any run path referenced them; they were kept alive only by their own tests and by each other.

Two of them defined operators whose names collide with live ones but whose implementations had diverged, so those tests were asserting behaviour the model does not have: the dead _interpolate_contravariant_vertical_velocity_to_full_levels has no concat_where and takes no nlev, so it does not handle the top level; the dead _compute_horizontal_kinetic_energy returns a 3-tuple where the live one returns a single field. Four numpy reference helpers from the deleted tests did have live consumers and were relocated rather than removed.

Not in scope

Narrowing vertical_cfl to KHalfDim: (max(2, nrdmax-2), nlev-3) is left open — it needs the host reduction slice narrowed in step, which is a decision that has not been made. No performance measurement or tuning.

Review follow-up

Three further commits address the review comments.

The vertical wind tendency is computed as three named contributions rather than accumulated through operators that each took the running total as an argument. _add_interpolated_horizontal_advection_of_w and _add_extra_diffusion_for_w_con_approaching_cfl now compute their contribution and are renamed _compute_*, with the cfl_clipping & owner_mask gate moved to the call site; _add_vertical_advection_of_w_to_advective_vertical_wind_tendency never added anything and is _compute_vertical_advection_of_w.

This is numerically unchanged including under mixed precision, which is the reason the additions stay sequenced instead of collapsing into one sum: ICON's ddt_w_adv_pc is a REAL(vp) array that is assigned once and then accumulated into, so each contribution is rounded to vpfloat before the next is added. Hoisting those conversions would drop roundings ICON performs. A TODO in the source asks whether that was deliberate on the Fortran side.

Computing the CFL is separated from clipping w. _compute_maximum_cfl_and_clip_contravariant_vertical_velocity did both and returned a three-tuple; it is split into _compute_cfl and _clip_contravariant_corrected_w, so the enclosing operator reads as correct w, compute the clipping, clip. The clip now sits outside the concat_where that restricts the CFL to the damping range — behaviour-preserving because cfl_clipping is False outside it, so both wheres degenerate to passing the velocity through, which is what the else-branch returned. _compute_horizontal_advection_of_w also moves next to the interpolation that is its only consumer.

One tangential wind stencil instead of two. _compute_tangential_wind_vp was _compute_tangential_wind_wp wrapped in a single astype, which made a usage search return a second definition rather than a consumer. There is now one operator and one program, both working precision, and the three call sites that want variable precision do the conversion where a reader can see it. The compute_tangential_wind_vp program goes with the operator it wrapped — a program body is a field operator call with out= and domain=, so the astype cannot live there; nothing outside its own stencil test used it.

Two review points remain open: finding a pattern that avoids passing tangential_wind_on_half_levels in purely to pass it through under skip_compute_predictor_vertical_advection, which looks like it needs GT4Py support rather than a local change; and whether the three CFL steps should be inlined at both call sites rather than kept in a shared wrapper.


🤖 Generated with Claude Code

https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF

havogt and others added 6 commits September 4, 2026 11:28
`run_predictor_step` issued three program calls and `run_corrector_step`
two; each is now a single program with one `field_operator` call and a
per-output domain.

The predictor merge makes `horizontal_advection_of_w_at_edges_on_half_levels`
(ICON `z_v_grad_w`) internal and folds the top-level extrapolation of
`vn_on_half_levels` into the same call; the corrector merge, together with the
predictor one, makes `contravariant_corrected_w_at_cells_on_model_levels`
(ICON `z_w_con_c_full`) internal. `VelocityAdvection` therefore allocates one
local field instead of three, and the predictor's vertical-momentum operator
now computes the horizontal advection of w inline, like the corrector's.

Every per-output domain is the one its predecessor program used; the two
merged fields' extents are now derived by GT4Py from the neighbour reads of
their consumers.

`compute_diagnostics_from_normal_wind`,
`compute_advection_in_predictor_vertical_momentum`,
`compute_advection_in_corrector_vertical_momentum` and
`compute_advection_in_horizontal_momentum` have no caller left and are
removed, together with their stencil tests; their `field_operator`s stay.
The savepoint comparisons those tests made against `z_vt_ie`, `z_kin_hor_e`
and `z_w_concorr_me` move to `test_velocity_predictor_step`, and the
`cfl_clipping` comparison to both component tests. `z_v_grad_w` and
`z_w_con_c_full` are no longer program fields, so their comparisons are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
`_compute_diagnostics_from_normal_wind` wrote
`contravariant_correction_at_edges_on_model_levels` (ICON `z_w_concorr_me`)
through a `concat_where(nflatlev <= KDim, computed, itself)`, which made the
field an input of the statement that writes it. Writing it over
`KDim: (nflatlev, vertical_end)` instead expresses the same result without the
aliasing, so one of the two remaining in-place statements in the merged
predictor is gone.

Below `nflatlev` the field is now left untouched rather than written back
unchanged. ICON computes it over `jk = nflatlev_jg, nlev`
(mo_velocity_advection.f90:279) and reads it only there, in
`_interpolate_contravariant_correction_to_cells_on_half_levels` and in
`vertically_implicit_dycore_solver`, both of which start at half level
nflatlev+1 and so reach model level nflatlev at the lowest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
`skip_compute_predictor_vertical_advection=True`,
`apply_extra_diffusion_on_vn=False`, `compile_time_vertical` fails to compile on
`dace_cpu`: `SplitMemlet.can_be_applied` reaches
`assert read_right or read_left` in
`gt4py/next/program_processors/runners/dace/transformations/splitting_tools.py:515`,
where both flags are `(a < b) == True` on SymPy relationals. With the horizontal
bounds symbolic and the vertical ones static neither comparison is decidable, so
both are False and a predicate that must return a bool asserts instead.

The model is unaffected: `setup_program` binds horizontal and vertical bounds
together, so `VelocityAdvection` only ever compiles the fully-static shape, which
passes on `dace_cpu`. The marker is conditioned on the mechanism -- vertical
static, horizontal not, on a dace backend -- rather than on the variant's name,
and is `strict` so it fails once GT4Py stops asserting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Both merged programs wrote the tendency over `KHalfDim: (vertical_start,
vertical_end)` and then restricted it inside the operator with two
`concat_where(1 <= KHalfDim, ..., vertical_wind_advective_tendency)` guards. The
output domain `(vertical_start + 1, vertical_end)` says the same thing once, at
the level where GT4Py can act on it.

ICON computes the field over `jk = 2, nlev` (mo_velocity_advection.f90:598 and
:613), i.e. half-open 0-based `(1, nlev)`, so this narrows *to* ICON's range
rather than away from it. The trailing
`_add_extra_diffusion_for_w_con_approaching_cfl` was never guarded, but it is
`where(cfl_clipping & owner_mask, ..., ddt_w_adv)` and `cfl_clipping` is False
below `max(2, end_index_of_damping_layer - 2)`, so the top half level was
previously written back unchanged and is now simply not written.

Collapsing the guards leaves the operator with no read of the incoming tendency
-- the first statement assigns rather than accumulates, matching the Fortran --
so it stops being an input. The corrector's aliasing therefore goes away
entirely; the predictor keeps its alias only because the compile-time
`skip_compute_predictor_vertical_advection` branch needs an else value.

Note the axis is `KHalfDim`, not `KDim`: the field became `CellKHalfField` with
the staggering change in #1429.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Emptying `compute_advection_in_vertical_momentum_equation.py`,
`compute_advection_in_horizontal_momentum_equation.py` and
`compute_diagnostics_from_normal_wind.py` of their programs left 17 field
operators behind, 15 of which have no consumer outside the two merged
programs. Those 15 move into `compute_velocity_advection.py`; the first two
modules held nothing else and are deleted.

`_interpolate_to_half_levels` and `_compute_horizontal_kinetic_energy` stay in
`compute_diagnostics_from_normal_wind.py`, which is now 36 lines, because
`compute_horizontal_velocity_quantities.py` imports them.

Pure code motion: the generated C++ for a given variant is byte-identical
across this commit, and so is its build-cache key -- checked by building the
same program from both commits into two empty caches and diffing. Only GT4Py's
translation-cache key changes, since it fingerprints the program definition and
therefore where its operators are defined, so the first run after this commit
re-translates and the next one hits the cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Five modules -- interpolate_vn_and_vt_to_ie_and_compute_ekin_on_edges,
interpolate_vn_to_half_levels_and_compute_kinetic_energy_on_edges,
compute_horizontal_kinetic_energy,
compute_maximum_cfl_and_clip_contravariant_vertical_velocity and
interpolate_contravariant_vertical_velocity_to_full_levels -- are the
pre-fusion ports. Nothing in the model, the driver, the tools, the bindings,
the docs or the scripts references them; the first three are kept alive only by
each other and by their own stencil tests, the last two only by their tests.

Deleting their tests loses no coverage, because two of them assert behaviour
the model does not have. Their operators share names with live ones whose
implementations have since diverged:

  - the deleted `_interpolate_contravariant_vertical_velocity_to_full_levels`
    takes no `nlev` and has no `concat_where`, so it reads
    `z_w_con_c(KDim + 0.5)` at the bottom model level; the live one special-cases
    `KDim == nlev - 1`.
  - the deleted `_compute_horizontal_kinetic_energy` returns
    `(vn_ie, z_vt_ie, z_kin_hor_e)` and, as its own TODO says, assigns two
    fields the name does not mention; the live one returns the kinetic energy
    alone.

Four numpy references in the deleted tests did have live consumers and move
rather than go: `interpolate_vn_to_half_levels_numpy` and
`compute_horizontal_kinetic_energy_at_edges_numpy` to
`test_compute_velocity_advection.py`, and the two compositions built on them to
`test_compute_horizontal_velocity_quantities_and_fluxes.py`, their only
consumer. They are renamed off the deleted modules so no name outlives its
definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
havogt and others added 3 commits September 8, 2026 09:13
`_compute_advective_vertical_wind_tendency` accumulated into a running
tendency through operators that each took the partial result as an
argument. It now computes the three contributions first and adds them
afterwards, so each term has a name and the accumulation is visible in
one place.

Consequently the two operators that did the accumulating are reshaped to
compute their contribution instead, and renamed off `_add_`:
`_compute_interpolated_horizontal_advection_of_w` returns the C2E
interpolation, and `_compute_extra_diffusion_for_w` returns the
diffusion, with the `cfl_clipping & owner_mask` gate moved to the call
site. `_add_vertical_advection_of_w_to_advective_vertical_wind_tendency`
never added anything and is renamed `_compute_vertical_advection_of_w`.

Numerically unchanged, including under mixed precision: every
contribution is still rounded to `vpfloat` before the next is added,
because ICON's `ddt_w_adv_pc` is a `REAL(vp)` array that it assigns once
and then accumulates into. Hoisting those conversions would drop
roundings that ICON performs, so the additions stay sequenced rather than
collapsing into a single sum.

The two reshaped operators had no consumer besides this module, so their
single-use wrapper programs are deleted and their stencil tests now drive
the field operators directly, in `test_compute_velocity_advection.py`
alongside the programs that use them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
…l advection

`_compute_maximum_cfl_and_clip_contravariant_vertical_velocity` did two
things and returned a three-tuple of the clipped velocity, the clipping
mask and the CFL. It is split into `_compute_cfl`, which derives the mask
and the CFL, and `_clip_contravariant_corrected_w`, which applies them,
so the enclosing operator reads as its three steps: correct w, compute
the clipping, clip.

The clip now sits outside the `concat_where` that restricts the CFL to
the damping range rather than inside it. That is behaviour-preserving
rather than merely equivalent-looking: `cfl_clipping` is False outside
that range, so both `where`s in the clip degenerate to passing the
velocity through, which is exactly what the else-branch returned before.

`_compute_horizontal_advection_of_w` moves into
`_compute_advective_vertical_wind_tendency`, next to the interpolation
that is its only consumer. Both call sites lose the separate call; in the
predictor the two were already inside the same
`skip_compute_predictor_vertical_advection` branch, so the variant is
undisturbed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
`compute_tangential_wind` came in a `_wp` and a `_vp` flavour, where the
`_vp` operator was the `_wp` one wrapped in a single `astype`. The
duplication made the module look like it held two stencils and made a
usage search return a second definition rather than a consumer.

There is now one operator, `_compute_tangential_wind`, and one program,
`compute_tangential_wind`, both working precision. The three call sites
that wanted variable precision do the conversion themselves, which puts
it where a reader can see it.

The `compute_tangential_wind_vp` program is removed with the operator it
wrapped: a program body is a field operator call with `out=` and
`domain=`, so the `astype` cannot be expressed there. Nothing outside its
own stencil test used it, and the conversion it tested is now exercised
through the call sites. `compute_tangential_wind` stays, since tracer
advection runs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
@havogt

havogt commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

cscs-ci run default;MODEL_SUBPACKAGES=dycore:common:tracer_advection;SESSIONS=model

@havogt

havogt commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Overview of the current state https://claude.ai/code/artifact/1441c859-e2ed-49b0-a517-6613716af1e3

havogt and others added 4 commits September 8, 2026 18:48
The predictor and corrector had separate vertical-momentum operators that
differed in two ways: the predictor derived
`contravariant_correction_at_cells_on_half_levels` from the edge field
while the corrector took it as an argument, and only the predictor could
skip the advective tendency.

Both now call `_compute_advection_in_vertical_momentum`, which always
takes the correction pre-computed and carries
`skip_vertical_wind_advective_tendency`. The corrector passes `False`,
so its branch is folded away and it reads as the plain case it is; the
predictor computes the correction itself beforehand and keeps the
previous tendency at its own call site. The asymmetry is now confined to
the caller that has it, rather than duplicated in two operators.

The skipped branch yields a dummy that the predictor discards. Compiling
both programs on `dace_cpu` with `GT4PY_DEBUG=1` shows the dummy never
reaches the generated code: in the `skip=True` variant
`vertical_wind_advective_tendency` appears only in the function
signatures and is never written, so the field keeps its value without a
copy. The remaining zero-fills belong to the `nflatlev` branch of the
contravariant-correction interpolation and are present in every variant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
compute_velocity_advection.py had grown to 1288 lines holding both
programs and all 19 operators. It now splits along the only line that
matters here, which of the two programs reaches a definition:

  velocity_advection_terms.py      15 step-agnostic operators
  velocity_advection_predictor.py  the predictor program + its 3 operators
  velocity_advection_corrector.py  the corrector program + its 1 operator

The stencil tests are arranged the same way, with the numpy references
in test_velocity_advection_terms.py where both step files import them.

The partition is only this clean because the vertical-momentum operator
was unified first; before that the two steps had diverged versions and
the terms module would not have been shared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
The marker was strict, so the variant it covered now fails as
XPASS(strict) instead of passing.

The gt4py bug is unfixed; gt4py is still pinned at 1.2.2 and nothing
upstream changed. What changed is the code that hit it. Before the two
steps shared one vertical-momentum operator, the predictor's inner
operator took vertical_wind_advective_tendency as an input and, in the
skip branch, returned that input unchanged: an input-to-output
pass-through inside the operator, with symbolic horizontal and static
vertical bounds. That is the memlet SplitMemlet.can_be_applied could
not decide. The shared operator fills the skip branch instead, and the
pass-through moved out to the program call site.

A full dace_cpu run over the three velocity-advection stencil-test
files is green apart from that XPASS, so the trigger is gone rather
than relocated. Reproducing the gt4py bug needs 470ba5b.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Nothing in model/, bindings/ or tools/ imports it; only its own stencil
test does. Velocity advection uses the _without_levelmask sibling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF

@nfarabullini nfarabullini left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just some comments

)


def copy_to_half_levels_and_compute_kinetic_energy_at_top_numpy(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

these are incredibly long names, can you shorten?

else broadcast(
vpfloat("0.0"),
(dims.CellDim, dims.KHalfDim),
) # "0.0" is a dummy value and should never be used

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The comment doesn't match the action

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it does, but maybe the comment is not clear enough. See the corresponding comment in the predictor field_operator.

Comment on lines +298 to +304
end_index_of_damping_layer=end_index_of_damping_layer,
start_cell_lateral_boundary_level_4=grid.start_index(
cell_domain(h_grid.Zone.LATERAL_BOUNDARY_LEVEL_4)
),
end_cell_halo=grid.end_index(cell_domain(h_grid.Zone.HALO)),
start_edge_nudging_level_2=grid.start_index(edge_domain(h_grid.Zone.NUDGING_LEVEL_2)),
end_edge_local=grid.end_index(edge_domain(h_grid.Zone.LOCAL)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we make this cleaner and define these above?

nfarabullini and others added 10 commits September 11, 2026 12:08
…encils/velocity_advection_terms.py

Co-authored-by: Hannes Vogt <hannes@havogt.de>
Brings in #1462, which makes CellParams.area required, ahead of binding it in the VelocityAdvection constructor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Cell areas are static geometry. Bind them with the other constant
arguments that setup_program binds, next to area_edge, instead of
passing them to both run methods on every call.

The two asserts that existed only to narrow the Optional type of those
arguments go as well: CellParams.area has been required since #1462,
which the previous commit merged in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
SolveNonhydro is the only user of VelocityAdvection: it constructs it
once and calls its two run methods from its own predictor and corrector
steps. The class moves verbatim (244 lines, byte-identical) and
velocity_advection.py goes. The test now constructs the class from
solve_nonhydro, and the docs reference follows it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
SolveNonhydro now sets up and calls the two velocity-advection programs
itself, as it does its other programs. The setup moves into its
constructor, the vertical CFL field joins its local fields, and the two
run methods are inlined at their call sites. The dtime scale factors
and the max-vertical-CFL reduction become module-level functions that
the tests share.

The velocity-advection datatests now call the two programs directly, in
the style of the per-program datatests in test_solve_nonhydro.py, and
keep every ICON comparison. The test of the class's initial state is
removed with the class. The advection docs page rendered only the
removed method, so it is removed too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
vpfloat is a variable that set_precision assigns, so mypy rejects it as a
type. anyfloat is the type alias, and it is what IntermediateFields uses
for plain-Python field annotations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
gt4py formats the C++ it generates with clang-format whenever
`clang-format --version` succeeds, and the test environment installs
clang-format through icon4py-tools.

On every push of this branch, the gtfn_cpu dycore stencil job has been
cancelled partway through, after 16-21 minutes; on main the same job
passes in 29-44. The likely cause is the runner running out of memory
while it compiles the large merged programs and clang-format formats
their sources.

With CLANG_FORMAT_EXECUTABLE=/bin/false, gt4py registers no C++
formatter at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
Adds StencilTests for _compute_cfl, _clip_contravariant_corrected_w,
_compute_extra_diffusion, _compute_vertical_advection_of_w,
_interpolate_contravariant_vertical_velocity_to_full_levels and
_interpolate_to_half_levels.

Five of these field operators use only vertical offsets, so gt4py infers
a cartesian grid for them when they run as programs, and gtfn rejects the
neighbour connectivities in the offset provider. They are now declared
with grid_type=UNSTRUCTURED, as the other vertical-only stencils under
test already are (e.g. _compute_dwdz_for_divergence_damping).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
@havogt

havogt commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

cscs-ci run default;MODEL_SUBPACKAGES=dycore:common:tracer_advection;SESSIONS=model

Set CLANG_FORMAT_EXECUTABLE=/bin/false in the CSCS
.test_runner_base, which every CSCS test and benchmark job extends,
and in the GitHub tools-and-bindings workflow. The model-test
workflow already sets it.

gt4py formats the code it generates whenever
`$CLANG_FORMAT_EXECUTABLE --version` succeeds. CI never reads that
code, so the formatting only costs time and memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
@havogt

havogt commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

cscs-ci run default;MODEL_SUBPACKAGES=dycore:common:tracer_advection;SESSIONS=model

The value is ta.vpfloat, which is np.float32 in mixed precision and so
not a float.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017s66WxborSWhPes8eHxiSF
@github-actions

Copy link
Copy Markdown

When developing, you can test your changes on CSCS CI before merge with the default pipeline: cscs-ci run default. This will run a default subset of tests.

You can pass options to override pipeline variables, for example:

  • cscs-ci run default;BACKENDS=gtfn_cpu;LEVELS=unit
  • cscs-ci run default;MODEL_SUBPACKAGES=common:driver;SESSIONS=model

Avoid running the pipeline for all tests when you are developing.

Available options are:

  • SESSIONS: model, model_mpi, or tools (correspond to nox sessions)
  • MODEL_SUBSETS: datatest, basic, or stencils (correspond to nox session selections)
  • MODEL_SUBPACKAGES: subpackages for non-MPI tests (last component, e.g. diffusion, driver)
  • MODEL_MPI_SUBPACKAGES: subpackages for MPI tests (as above)
  • BACKENDS: backends
  • GRIDS: grids for stencil tests (simple, icon_regional, or icon_global)
  • LEVELS: testing level for non-stencil tests (unit or integration)

For each option, all can be used as a shorthand for all possible values of that variable, e.g. LEVELS=all.

Multiple values can be given to each option with : used as the separator (; separates options and , separates pipelines).

See scripts/python/generate_ci_pipeline.py and noxfile.py for available values for each option.

The all pipeline can be run with cscs-ci run all. This will run all icon4py tests in CSCS CI which can be expensive. This pipeline runs on a schedule on main, and can be run when extensive validation is needed (e.g. before releases).

Merging

Once your PR is approved and ready for merging, add it to the merge queue. The merge CSCS CI pipeline will run automatically on the merge-queue branch and must pass before the PR is merged. A dummy merge check will be triggered on the PR itself since it's required to add a PR to the merge queue.

Optional Tests

To run benchmarks you can use:

  • cscs-ci run benchmark-bencher

For more detailed information please look at CI in the EXCLAIM universe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants