Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion capgen/generator/group_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,7 @@ def _generate_state_alloc(suite_name: str, group_name: str) -> List[str]:
so when multiple instances initialize, only the first allocates and
initialises the state array; subsequent calls return immediately to
avoid clobbering peer-instance state slots. Matches the
``<suite>_suite_state_alloc`` pattern.
``suite_state_alloc`` pattern.
"""
# Short Fortran symbol; the module ``ccpp_<suite>_<group>_cap``
# already namespaces this routine at link time.
Expand Down
30 changes: 23 additions & 7 deletions capgen/generator/host_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from typing import Dict, List, Optional, Set, Tuple

from metadata.parse_tools import CCPPError, open_if_changed
from metadata.metadata_table import FORTRAN_MAX_IDENT_LEN
from metadata.variable_resolver import SchemeStore
from generator.suite_resolver import (
ResolvedArg,
Expand Down Expand Up @@ -1026,14 +1027,29 @@ def _generate_host_cap(
use_lines: List[str] = []
for sname in suite_names:
suite_cap_mod = 'ccpp_{}_cap'.format(sname)
# Suite cap symbols carry no suite prefix (issue #786), so every
# suite cap exports the same names. Rename on import to keep the
# N suites in this scope distinct.
suite_subs = []
suite_subs.append('{}_register'.format(sname))
suite_subs.append('{}_init'.format(sname))
for phase in _PHYSICS_PHASES:
suite_subs.append('{}_physics_{}'.format(sname, phase))
suite_subs.append('{}_final'.format(sname))
syms = ', '.join(suite_subs)
use_lines.append('{}use {}, only: {}'.format(_INDENT, suite_cap_mod, syms))
aliased = [('{}_register'.format(sname), 'suite_register'),
('{}_init'.format(sname), 'suite_init')]
aliased += [('{}_physics_{}'.format(sname, p), 'suite_physics_{}'.format(p))
for p in _PHYSICS_PHASES]
aliased.append(('{}_final'.format(sname), 'suite_final'))
for alias, target in aliased:
if len(alias) > FORTRAN_MAX_IDENT_LEN:
raise CCPPError(
"Suite name '{}' is too long: the host cap imports the "
"suite cap's '{}' as '{}' ({} characters), over Fortran's "
"{}-character limit for names".format(
sname, target, alias, len(alias), FORTRAN_MAX_IDENT_LEN
)
)
suite_subs.append('{} => {}'.format(alias, target))
use_lines.append('{}use {}, only: &'.format(_INDENT, suite_cap_mod))
for i, sym in enumerate(suite_subs):
sep = ', &' if i < len(suite_subs) - 1 else ''
use_lines.append('{}{}{}'.format(_INDENT * 2, sym, sep))
ensure_error_unit_use(use_lines, _INDENT)
lines.extend(use_lines)

Expand Down
86 changes: 54 additions & 32 deletions capgen/generator/suite_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
* Imports all group cap modules and the constituent property module.
* Exposes eight public entry points:

- ``<suite>_register`` — calls each scheme's ``_register`` to populate
the host-owned ``ccpp_model_constituents_t`` object.
- ``<suite>_init`` / ``<suite>_final`` — framework setup / teardown.
- ``<suite>_physics_init``, ``<suite>_physics_timestep_init``,
``<suite>_physics_run``, ``<suite>_physics_timestep_final``,
``<suite>_physics_final`` — dispatch by ``group_name`` to the
appropriate group cap subroutine.
- ``suite_register`` — calls each scheme's ``_register``.
- ``suite_init`` / ``suite_final`` — framework setup / teardown.
- ``suite_physics_init``, ``suite_physics_timestep_init``,
``suite_physics_run``, ``suite_physics_timestep_final``,
``suite_physics_final`` — dispatch by ``group_name`` to the
appropriate group cap subroutine. These carry no suite prefix: the
module name already supplies it, and repeating it overran Intel's
mangled-global-name limit for long suite names (issue #786). The
host cap renames them back to ``<suite>_<what>`` on import.

The static API (``<host>_ccpp_cap.F90``) dispatches by ``suite_name`` to
these subroutines.
Expand All @@ -23,7 +25,7 @@
import os
from typing import Dict, List, Optional, Set

from metadata.parse_tools import open_if_changed
from metadata.parse_tools import CCPPError, open_if_changed
from metadata.variable_resolver import HostVarEntry, SchemeStore
from generator.suite_resolver import (
ResolvedArg,
Expand Down Expand Up @@ -57,6 +59,12 @@
# Canonical set of physics phases, always dispatched by the suite cap.
_PHYSICS_PHASES = ('init', 'timestep_init', 'run', 'timestep_final', 'final')

_SUITE_CAP_OWN_SYMS = frozenset(
['suite_physics_{}'.format(_p) for _p in _PHYSICS_PHASES]
+ ['suite_register', 'suite_init', 'suite_final',
'suite_state_alloc', 'suite_state_dealloc']
)

# Constituent type / module constants.
_CONST_MOD = 'ccpp_constituent_prop_mod'
_CONST_DDT = 'ccpp_model_constituents_t'
Expand Down Expand Up @@ -405,7 +413,8 @@ def _register_lines(
Minimal signature: ``(instance_number, number_of_instances, errmsg,
errflg)`` (the instance pair is included only when the host declares it).
"""
sub_name = '{}_register'.format(suite_name)
sub_name = 'suite_register'
sub_label = '{}_register'.format(suite_name)
i1 = _INDENT
i2 = _INDENT * 2

Expand Down Expand Up @@ -482,7 +491,7 @@ def _register_lines(
# compilers don't flag intent(in) args as unused when the gate is off.
extra_in = [ninstances_local] if ninstances_local else None
trace_lines = emit_trace_block(
sub_name, [], i2,
sub_label, [], i2,
instance_local=inst_local, extra_in_names=extra_in,
)
if trace_lines:
Expand All @@ -497,7 +506,7 @@ def _register_lines(
]

# Allocate state and DDT array on first call (idempotent).
suite_alloc_sub = '{}_suite_state_alloc'.format(suite_name)
suite_alloc_sub = 'suite_state_alloc'
lines.append('{}call {}({}, {}, {})'.format(
i2, suite_alloc_sub, ninstances_arg, errmsg_local, errflg_local
))
Expand Down Expand Up @@ -652,7 +661,8 @@ def _init_lines(
Minimal signature: ``(instance_number, number_of_instances, errmsg,
errflg)`` -- the instance pair is included only when the host declares it.
"""
sub_name = '{}_init'.format(suite_name)
sub_name = 'suite_init'
sub_label = '{}_init'.format(suite_name)
i1 = _INDENT
i2 = _INDENT * 2

Expand Down Expand Up @@ -704,7 +714,7 @@ def _init_lines(
]
extra_in = [ninstances_local] if ninstances_local else None
trace_lines = emit_trace_block(
sub_name, [], i2,
sub_label, [], i2,
instance_local=inst_local, extra_in_names=extra_in,
)
if trace_lines:
Expand All @@ -718,7 +728,6 @@ def _init_lines(
]

# State guard: must be in REGISTERED state (or already INITIALIZED — idempotent).
sub_label = '{}_init'.format(suite_name)
lines += [
'{}if (.not. allocated(ccpp_suite_state)) then'.format(i2),
"{} {} = '{}: ccpp_register has not been called'".format(
Expand Down Expand Up @@ -811,7 +820,8 @@ def _final_lines(
symmetry with ``<suite>_register`` / ``<suite>_init``; the framework
does not consume it at final time.
"""
sub_name = '{}_final'.format(suite_name)
sub_name = 'suite_final'
sub_label = '{}_final'.format(suite_name)
i1 = _INDENT
i2 = _INDENT * 2

Expand Down Expand Up @@ -865,7 +875,7 @@ def _final_lines(
]
extra_in = [ninstances_local] if ninstances_local else None
trace_lines = emit_trace_block(
sub_name, [], i2,
sub_label, [], i2,
instance_local=inst_local, extra_in_names=extra_in,
)
if trace_lines:
Expand Down Expand Up @@ -926,7 +936,7 @@ def _final_lines(
i2, dealloc_sub, errmsg_local, errflg_local
))
lines.append('{} if ({} /= 0) return'.format(i2, errflg_local))
suite_dealloc_sub = '{}_suite_state_dealloc'.format(suite_name)
suite_dealloc_sub = 'suite_state_dealloc'
lines.append('{} call {}({}, {})'.format(
i2, suite_dealloc_sub, errmsg_local, errflg_local
))
Expand Down Expand Up @@ -958,15 +968,16 @@ def _physics_dispatch_lines(
suite_res: SuiteResolution,
host_dict=None,
) -> List[str]:
"""Generate a ``<suite>_physics_<phase>`` dispatch subroutine.
"""Generate a ``suite_physics_<phase>`` dispatch subroutine.

The subroutine signature is derived entirely from the host's ``type=control``
metadata (all control variables except ``suite_name``, which is consumed at the
static API dispatch level). When ``group_name`` is in the control table the
body uses a ``select case`` dispatch; otherwise all groups are called
unconditionally.
"""
sub_name = '{}_physics_{}'.format(suite_name, phase)
sub_name = 'suite_physics_{}'.format(phase)
sub_label = '{}_physics_{}'.format(suite_name, phase)
i1 = _INDENT
i2 = _INDENT * 2
i3 = _INDENT * 3
Expand Down Expand Up @@ -1014,7 +1025,7 @@ def _physics_dispatch_lines(

# Trace block: references every intent(in)/inout control dummy so that
# strict compilers don't flag any of them as unused.
trace_lines = emit_trace_block(sub_name, ctrl_entries, i2)
trace_lines = emit_trace_block(sub_label, ctrl_entries, i2)
if trace_lines:
lines.append('')
lines.extend(trace_lines)
Expand All @@ -1031,7 +1042,6 @@ def _physics_dispatch_lines(
lines.append('')

inst_idx = _instance_idx(host_dict)
sub_label = '{}_physics_{}'.format(suite_name, phase)
if phase == 'final':
# ``physics_final`` is silently idempotent: a repeat call (or a
# call issued after ``ccpp_final``) must return cleanly with
Expand Down Expand Up @@ -1107,7 +1117,6 @@ def _emit_group_call(resolved_group, indent):
# control table there's nowhere to write the message, so skip
# emission rather than silently swallow.
if errflg_local and errmsg_local:
sub_label = '{}_physics_{}'.format(suite_name, phase)
lines.append('{}case default'.format(i2))
lines.append('{}{} = 1'.format(i3, errflg_local))
lines.append(
Expand All @@ -1131,15 +1140,15 @@ def _suite_state_alloc_lines(
suite_name: str,
has_suite_vars: bool,
) -> List[str]:
"""Generate the ``<suite>_suite_state_alloc`` subroutine.
"""Generate the ``suite_state_alloc`` subroutine.

Idempotent allocator for the per-instance suite state array and the
suite-owned DDT array. Inner allocatable fields inside the DDT are NOT
allocated here — that happens in ``ccpp_<suite>_suite_data_init_fields``,
called from ``<suite>_init`` after register-phase scheme calls have set
any suite-owned scalar dimensions.
"""
sub_name = '{}_suite_state_alloc'.format(suite_name)
sub_name = 'suite_state_alloc'
data_alloc = 'suite_data_alloc'
data_mod = 'ccpp_{}_data'.format(suite_name)
i1 = _INDENT
Expand Down Expand Up @@ -1178,8 +1187,8 @@ def _suite_state_dealloc_lines(
suite_name: str,
has_suite_vars: bool,
) -> List[str]:
"""Generate the ``<suite>_suite_state_dealloc`` subroutine."""
sub_name = '{}_suite_state_dealloc'.format(suite_name)
"""Generate the ``suite_state_dealloc`` subroutine."""
sub_name = 'suite_state_dealloc'
data_dealloc = 'suite_data_dealloc'
data_mod = 'ccpp_{}_data'.format(suite_name)
i1 = _INDENT
Expand Down Expand Up @@ -1264,6 +1273,19 @@ def _generate_suite_cap(
]
syms_list.append('{}_state_alloc'.format(resolved_group.group_name))
syms_list.append('{}_state_dealloc'.format(resolved_group.group_name))
# This module's own symbols are suite-name-free (issue #786), so a
# group named e.g. ``suite`` or ``suite_physics`` would export the
# same names it defines.
clash = sorted(set(syms_list) & _SUITE_CAP_OWN_SYMS)
if clash:
raise CCPPError(
"Suite '{}' has a group named '{}' whose cap exports {}, "
"colliding with the suite cap's own subroutine(s); rename "
"the group".format(
suite_name, resolved_group.group_name,
', '.join(repr(c) for c in clash),
)
)
use_lines.append('{}use {}, only: {}'.format(
_INDENT, group_cap_mod, ', '.join(syms_list)
))
Expand All @@ -1281,13 +1303,13 @@ def _generate_suite_cap(
# points are always emitted. ``ccpp_register`` is mandatory in the new
# design — even an empty register phase fires the state transition.
pub_subs = []
pub_subs.append('{}_register'.format(suite_name))
pub_subs.append('{}_init'.format(suite_name))
pub_subs.append('suite_register')
pub_subs.append('suite_init')
for phase in _PHYSICS_PHASES:
pub_subs.append('{}_physics_{}'.format(suite_name, phase))
pub_subs.append('{}_final'.format(suite_name))
pub_subs.append('{}_suite_state_alloc'.format(suite_name))
pub_subs.append('{}_suite_state_dealloc'.format(suite_name))
pub_subs.append('suite_physics_{}'.format(phase))
pub_subs.append('suite_final')
pub_subs.append('suite_state_alloc')
pub_subs.append('suite_state_dealloc')

for sub in pub_subs:
lines.append('{}public :: {}'.format(_INDENT, sub))
Expand Down
20 changes: 17 additions & 3 deletions doc/briefing.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ The headline items for a reader of this brief:
gating the framework setter additions. `followups.md` FU-020, FU-003.
- **Codegen-time scheme-registration cross-check** — today's check is at
runtime. FU-002.
- **Group-cap symbol-length budget** — the suite cap is guarded and
tested at its ceiling; the group cap's joint `suite`/`group` budget
is not. FU-037.
- **What the constituent metadata flags may legally be attached to** —
only a deliberately thin guard is enforced today (`index_of_*` may
not carry one). A stricter shape rule was written and rejected: it
would have encoded today's constituent storage in the metadata
parser. FU-036.
- **Nested-subcycle `ccpp_loop_counter` semantics** — resolves to the
outermost counter. FU-001.
- **Transient shims** — `--legacy-mode`, `--gfs-dim-aliases`,
Expand Down Expand Up @@ -417,8 +425,8 @@ don't rebuild downstream objects unless something actually moved.
- The three incoming hosts **still rely on the transient migration
shims** (§6.3b). Adoption did *not* retire them — it pinned
them. See the note in that section and FU-010/FU-011.
- **Unit tests**: 1564 passing on `feature/capgen-v1` (verified
2026-09-01, `python unit-tests/run_tests.py`).
- **Unit tests**: 1577 passing on `feature/capgen-v1` (verified
2026-09-16, `python unit-tests/run_tests.py`).
- **End-to-end tests passing** (13): `advection`,
`advection_auto_clone`, `capgen`, `capgen_ng`, `chunked_data`,
`constituents_dim`, `ddthost`, `instances`, `instances_advection`,
Expand Down Expand Up @@ -484,7 +492,13 @@ don't rebuild downstream objects unless something actually moved.
served (§1). The anticipated complication was "fast physics"
called directly from the FV3 dynamical core as a separate group;
**that group works with v1 as expected**, so no special handling
was needed.
was needed. First production defect report arrived 2026-09-16
(issue #786): Intel rejected the generated suite-cap symbols as
`Global name too long` under `WARN_AS_ERROR`. The code was
standard-conforming, but capgen v1 had spent 11 characters of
linker-symbol budget that `ccpp-prebuild` did not, so a suite name
that used to build no longer did. Fixed the same day by removing
the suite name from the suite-cap subroutines; `followups.md` §5.
- **CAM-SIMA**: **still on capgen v0 — the remaining transition, and
the critical path.** As of 2026-09-01 CAM-SIMA production builds
with the original ccpp-capgen; capgen v1 support lives on
Expand Down
5 changes: 3 additions & 2 deletions doc/briefing_pm.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ Features that exist only in capgen (some exist in prebuild):
and the retirement of the CAM-SIMA compatibility layer) to
programme-level blockers rather than CAM-SIMA-local work. Schedule
risk concentrates there; see §8.
- **Unit tests**: 1564 passing. No known failures.
- **Unit tests**: 1577 passing. No known failures.
- **End-to-end tests**: 13 passing — `advection`,
`advection_auto_clone` (CAM-SIMA advection_test port exercising the
auto-clone shim), `capgen`, `capgen_ng`, `chunked_data`,
Expand Down Expand Up @@ -392,9 +392,10 @@ proposals are implementable on top of it.
| capgen diverges from capgen feature set | LOW | Cross-checked by `doc/redesign_analysis.md`; the feature comparison table in §4 / §5 is exhaustive |
| Host metadata break for UFS / NEPTUNE / CAM-SIMA | LOW | Three transient shims (`--legacy-mode`, `--gfs-dim-aliases`, `--legacy-auto-clone-constituents`) together cover the known-incompatible standard-name pair, the GFS radiation/composition vertical-dim spellings, and original capgen's auto-clone registration path. Remaining required changes (e.g., `_finalize` → `_final`) are mechanical and listed in `doc/migration.md` §3 |
| Constituent overhaul stalls | LOW | Proposal A unblocks the immediate bug; capgen works with the current framework today; `--legacy-auto-clone-constituents` lets CAM-SIMA's atmospheric_physics build without an overhaul decision; the overhaul is a separate decision track |
| Bus-factor on capgen itself | MEDIUM | Procedural code style + flat data classes + 1564-test safety net; significantly lower than capgen's bus factor |
| Bus-factor on capgen itself | MEDIUM | Procedural code style + flat data classes + 1577-test safety net; significantly lower than capgen's bus factor |
| **CAM-SIMA transition slips, delaying the whole programme** | **MEDIUM — the main schedule risk as of 2026-09-01** | The `develop` merge and the deletion of both older generators are gated on this one transition (§6), so its two remaining items — the constituent-ordering re-baseline and retirement of the compatibility layer — are programme-level blockers. Both are understood and scoped; neither is a defect. Mitigation is to track them as such rather than as CAM-SIMA-local work, and to decide the auto-clone shim's fate as part of the transition |
| Two host call-shape conventions (prebuild-style vs capgen-style) coexist forever | LOW | capgen emits one shape; downstream host conversions are tracked in `doc/migration.md` |
| First production defect from a transitioned host | CLOSED (2026-09-16) | UFS reported issue #786 — Intel rejected generated symbol names as too long when the UFS build turned on warnings-as-errors. Diagnosed, fixed and tested the same day. Notable for two reasons: the generated code was standard-conforming (an Intel linker limit, not a Fortran violation), **and** it was a regression against `ccpp-prebuild`, which had used shorter names — so the transition, not the compiler, was the trigger. The class is now guarded by a test that generates at the supported limit rather than at any one model's suite name |
| Regression discovered during NEPTUNE / UFS testing | LARGELY RETIRED (2026-09-01) | Both models have transitioned; NEPTUNE passes ~300 regression tests on three compilers including high-altitude physics, and the anticipated UFS FV3 fast-physics complication did not materialise. The SCM proving-ground approach worked as intended — issues became capgen fixes, not host-side patches |
| ccpp-prebuild end-of-life requires a sunset plan | SCOPED (2026-09-01) | Decided: `ccpp-prebuild` **and** the older `ccpp-capgen` are both deleted from the framework repo in the same operation that merges capgen into `develop`, triggered when/after CAM-SIMA transitions. prebuild already has no production consumer. The residual risk is schedule, not scope — see the CAM-SIMA row above |

Expand Down
Loading
Loading