Four fixes from reviewing the spec builder - #950
Merged
Conversation
Build cost — v1 vs legacyv1 build peak & time relative to legacy, on this commit — not a comparison against master (that is CodSpeed).
Full table (time + peak, mean)📊 Interactive plots + CSV: download the semantics-report-v1-vs-legacy artifact from this run. Report-only · not a gate · refreshed on every push · obsolete once legacy is dropped. |
4 tasks
FBumann
force-pushed
the
spec-parameter-ownership
branch
from
September 8, 2026 17:24
041ab2d to
4d4a508
Compare
`restamp_coords` puts a spec's master coordinates back on every container carrying a dimension of that name, so the whole model agrees on one dtype per dimension however the netcdf engine returned it. It asked only whether the dtypes differed, not whether the labels were the same -- and a spec-built model may hold hand-added variables of its own. So a variable added on `snapshot = [10, 11, 12]` beside a spec built on `[0, 1, 2]` came back relabelled to the spec's, silently; and one on a `snapshot` of another length failed the read outright with `conflicting sizes for dimension 'snapshot'`. The `CSRConstraint` branch replaced its `Grid` indexes unconditionally, without even the dtype guard. Both went unseen on netcdf4, which preserves the dtype; scipy narrowing int64 to int32 is what makes an index look stale. An index is now restamped only where it holds the master's own labels, which is what `Index.equals` asks -- it compares labels and not dtypes, so a narrowed int and a widened bool still match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZfHfTFENUy6WxnFFri5Td
`grouped_sum` promises that "a group no member reaches holds the empty sum, which is 0 and not an absence", and delivers it by filtering the operand down to its mapped members and grouping what is left. Where a lookup maps no member at all, what is left is empty -- and an empty dimension is one xarray refuses to group over, so the promise came out as `ValueError: s must not be empty` from inside the groupby, on the fold and at build time alike. The empty case is now answered directly, the way `sum_over` already answers a term beside an empty dimension: zeros over the operand's remaining dimensions and every declared label of the group, as data or as a constant term. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZfHfTFENUy6WxnFFri5Td
`attach` lets a dimension the spec declares but no declaration reaches go without a source, deliberately, so it never lands in `coords`. The `ModelSpec` repr indexed `coords[d]` for every declared dimension anyway, so a model that built perfectly well could not be looked at: `repr(model.spec)` raised `KeyError` in a REPL or a notebook. Such a dimension is now shown as `unreached`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZfHfTFENUy6WxnFFri5Td
`retain="all"` reaches every declared parameter, including ones no declaration reads, and it did so after `build()` had already put the variables and constraints on the model. A parameter with no source therefore raised with the model half-built and `_spec` still unset -- and the corrected retry was then refused by the guard that `add_spec` builds into an empty model, so the only way on was a fresh model. The retained set is resolved first. It needs nothing the build produces, so the failure now lands before anything is added and the same model takes the corrected sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZfHfTFENUy6WxnFFri5Td
FBumann
force-pushed
the
spec-review-fixes
branch
from
September 8, 2026 17:24
3dadc43 to
435bb27
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
Stacked on #949. Four bugs found reviewing #922, one commit each. Every one is
reproduced by a test that fails on the parent commit and passes on the fix.
1.
read_netcdfrelabels a hand-added variable (fix(io))restamp_coordsputs a spec's master coordinates back on every containercarrying a dimension of that name, so the whole model agrees on one dtype per
dimension however the engine returned it. It asked only whether the dtypes
differed, never whether the labels were the same — and a spec-built model may
hold hand-added variables of its own.
snapshot = [10, 11, 12]beside a spec built on[0, 1, 2]came back silently relabelled to the spec's;
snapshotof another length failed the read withValueError: conflicting sizes for dimension 'snapshot'.The
CSRConstraintbranch rebuilt itsGridunconditionally, without even thedtype guard. Both go unseen on netcdf4, which preserves the dtype; it is scipy
narrowing int64 to int32 that makes an index look stale.
An index is now restamped only where it holds the master's own labels, which is
what
Index.equalsasks — it compares labels and not dtypes, so a narrowed intand a widened bool still match.
2. A lookup that maps nothing (
fix(spec))grouped_sumpromises that "a group no member reaches holds the empty sum,which is 0 and not an absence", and delivers it by filtering the operand down to
its mapped members and grouping what is left. Where a lookup maps no member at
all, what is left is empty — and an empty dimension is one xarray refuses to
group over, so the promise came out as
ValueError: s must not be emptyfrominside the groupby, on the fold and at build time alike.
The empty case is now answered directly, the way
sum_overalready answers aterm beside an empty dimension: zeros over the operand's remaining dimensions
and every declared label of the group, as data or as a constant term.
3.
repr(model.spec)on an unreached dimension (fix(spec))attachlets a dimension the spec declares but no declaration reaches gowithout a source, deliberately (
test_unreached_dimension_needs_no_source), soit never lands in
coords. The repr indexedcoords[d]for every declareddimension anyway — so a model that built perfectly well could not be looked at
in a REPL or a notebook. Shown as
unreachednow.4. A half-built model on a retain failure (
fix(spec))retain="all"reaches every declared parameter, including ones no declarationreads, and did so after
build()had already put the variables andconstraints on the model. A parameter with no source therefore raised with the
model half-built and
_specstill unset — and the corrected retry was thenrefused by the guard that
add_specbuilds into an empty model, so the only wayon was a fresh model. The retained set needs nothing the build produces, so it
is resolved first.
Verification (3dadc43)
test/remoteis excluded: its OETC cloud-provider tests fail identically on thebase commit in this environment and touch nothing here.
Each fix was also checked in isolation by stashing only the source change and
re-running its test:
62860f72test_a_hand_added_variable_keeps_its_own_labelsfd0f2669test_a_lookup_that_maps_nothing_leaves_every_group_at_the_empty_sumValueError: s must not be empty673eb6b1test_a_declared_dimension_with_no_source_still_reprsKeyError3dadc434test_a_build_that_cannot_retain_leaves_the_model_buildablepandpower_balanceChecklist
AGENTS.md).doc/release_notes.rstis included.🤖 Generated with Claude Code
https://claude.ai/code/session_015ZfHfTFENUy6WxnFFri5Td