Merge VelocityAdvection into two programs with per-output domains - #1460
Merge VelocityAdvection into two programs with per-output domains#1460havogt wants to merge 25 commits into
Conversation
`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
`_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
|
cscs-ci run default;MODEL_SUBPACKAGES=dycore:common:tracer_advection;SESSIONS=model |
|
Overview of the current state https://claude.ai/code/artifact/1441c859-e2ed-49b0-a517-6613716af1e3 |
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
| ) | ||
|
|
||
|
|
||
| def copy_to_half_levels_and_compute_kinetic_energy_at_top_numpy( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
The comment doesn't match the action
There was a problem hiding this comment.
I think it does, but maybe the comment is not clear enough. See the corresponding comment in the predictor field_operator.
| 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)), |
There was a problem hiding this comment.
can we make this cleaner and define these above?
…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
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
|
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
|
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
|
When developing, you can test your changes on CSCS CI before merge with the You can pass options to override pipeline variables, for example:
Avoid running the pipeline for all tests when you are developing. Available options are:
For each option, Multiple values can be given to each option with 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. |
Part of the Audit ICON4Py programs with new GT4Py features project. First unit of work:
VelocityAdvectionin its final shape.What this is for
A
field_operatorcall 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
VelocityAdvectionissued 5 program calls across its two run methods; it now issues 2, each a singlefield_operatorcall 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_fieldsgoes from 3 fields to 1.horizontal_advection_of_w_at_edges_on_half_levels(ICONz_v_grad_w) andcontravariant_corrected_w_at_cells_on_model_levels(ICONz_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 ofwinline, which is how the corrector already did it.Both host
max_vertical_cflreductions moved after the fused call — safe becauseapply_extra_diffusion_on_vn = Trueis 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.f90rather than from the previous icon4py bounds:contravariant_correction_at_edges_on_model_levelsKDim: (nflatlev, nlev)DO jk = nflatlev_jg, nlevvertical_wind_advective_tendency(both programs)KHalfDim: (1, nlev)DO jk = 2, nlevvn_on_half_levelsKHalfDim: (0, nlev+1)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/E2Creads 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_operatorthey used survives and is exercised through the merged programs.Verification
test_compute_velocity_advection.pygtfn_cputest_compute_velocity_advection.pydace_cputest_velocity_advection.py --datatest-onlygtfn_cpu/dace_cputest_solve_nonhydro.py(4 dycore cases)gtfn_cpu/dace_cpuNo
--skip-stenciltest-verificationanywhere. 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_wandz_w_con_c_full. To compensate,z_vt_ie,z_kin_hor_e,z_w_concorr_meand acfl_clippingmask comparison were added to the component tests, where none of them existed before.Known issues
One xfail,
strict=True. Onedace_cpuvariant hitsassert read_right or read_leftinsplitting_tools.py:515, reached fromSplitMemlet.can_be_applied: both flags come from(a < b) == Trueon 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 on4a064f23c. The model is unaffected:setup_programbinds 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 leavesz_vt_ieuntouched while ICON sets half level 0 unconditionally (mo_velocity_advection.f90:300, outside theIF (.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.pyloses 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 intocompute_velocity_advection.py(now 1300 lines, 17 operators + 2 programs).compute_advection_in_{vertical,horizontal}_momentum_equation.pyare deleted;compute_diagnostics_from_normal_wind.pyis down to 36 lines, holding the two operatorscompute_horizontal_velocity_quantities.pystill 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 bydiffagainst a separate clean build cache.Separately, five superseded modules are deleted with their stencil tests: the old
_mo_velocity_advection_stencil_05/14/15ports (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_levelshas noconcat_whereand takes nonlev, so it does not handle the top level; the dead_compute_horizontal_kinetic_energyreturns 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_cfltoKHalfDim: (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_wand_add_extra_diffusion_for_w_con_approaching_cflnow compute their contribution and are renamed_compute_*, with thecfl_clipping & owner_maskgate moved to the call site;_add_vertical_advection_of_w_to_advective_vertical_wind_tendencynever 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_pcis aREAL(vp)array that is assigned once and then accumulated into, so each contribution is rounded tovpfloatbefore the next is added. Hoisting those conversions would drop roundings ICON performs. ATODOin 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_velocitydid both and returned a three-tuple; it is split into_compute_cfland_clip_contravariant_corrected_w, so the enclosing operator reads as correct w, compute the clipping, clip. The clip now sits outside theconcat_wherethat restricts the CFL to the damping range — behaviour-preserving becausecfl_clippingisFalseoutside it, so bothwheres degenerate to passing the velocity through, which is what the else-branch returned._compute_horizontal_advection_of_walso moves next to the interpolation that is its only consumer.One tangential wind stencil instead of two.
_compute_tangential_wind_vpwas_compute_tangential_wind_wpwrapped in a singleastype, 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. Thecompute_tangential_wind_vpprogram goes with the operator it wrapped — a program body is a field operator call without=anddomain=, so theastypecannot 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_levelsin purely to pass it through underskip_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