Act on independent review: unit scale correctness, grid guard, lenient display inference - #34
Merged
Merged
Conversation
…ference
An independent reviewer went through the units feature hunting for bugs and
tried to write a recipe for their own model from the docs alone. Four findings
were worth fixing, plus the doc gap that stopped them cold.
Correctness
- The stamped `units` on the ECCO salt budget were wrong by 1000x. ECCO's salt
fluxes carry the salinity scale in their own units string (`1e-3 m3 s-1`), and
the recipe's compensating `unit_conversion: 0.001` was a bare number — so the
scale survived into the composed units while the values were already kg/s.
Anyone reading the attribute got a thousandth of the real flux. The conversion
constants now declare the reciprocal scale (`{value: 0.001, units: "1e3"}`,
and `kg g-1` for the g->kg one), so value x units == 1 and the scale cancels.
Values are untouched; only the label changed.
- `assert_unit_consistency` now compares magnitude, not just dimension, so it
catches that class of error instead of passing it. `is_convertible` cannot see
a numeric scale, which is exactly what a mis-declared conversion factor leaves
behind. Both shipped models reconcile exactly under the stricter check. This
reverses the "dimension, not exact units" decision documented in #32; the
motivating case there (salinity's `1e-3`) is better handled by making the
recipe cancel the scale than by declining to look at it.
- `lateral_divergence` without an `xgcm.Grid` raised `AttributeError: 'NoneType'
object has no attribute 'axes'` — a regression from the rechunking in #33,
which reads the grid before the guarded helper is called. Both grid-needing
operations now share one `_require_grid`, and the divergence has the
grid-guard test `difference` already had.
- `show_recipe` blanked the units of every term above a missing diagnostic,
including the budget roots — on the raw ECCO dataset, all three rhs roots.
`infer_units` takes an optional `available` predicate and, given one, mirrors
what the evaluator builds (dropping operands it would drop) rather than what
the recipe describes. A term that genuinely will not be built is still blank.
Smaller fixes
- A `constants` entry missing `value` raised a bare `KeyError`; a budget named
`constants` raised the same. Both now say what is wrong, with the path.
- The `reciprocal` operand was the one leaf the HTML tree left unannotated.
- Missing diagnostics are reported before unit issues: an absent input causes
unknown units, so `on_missing="raise"` now names the diagnostic, not the
symptom.
- `units:` on a term gets its own warning instead of the generic "missing an
enclosing product" hint, which pointed at the wrong fix.
- Rechunking is a no-op on unchunked data rather than silently returning dask.
Docs
- `recipes.md` gained the `xgcm.Grid` section it never had. The reviewer got
through everything else from the docs and then had to read `tests/conftest.py`
to learn that xgcm >= 0.10 wants `padding=`, which every older tutorial spells
`boundary=`/`periodic=`.
- The "conversion factors are dimensionless" guidance led them straight into the
bug above; it now says the opposite, with the worked pattern.
- Documented what the assertion does not check, that constants substitute only
in sum/product operands, and that `aggregate()` returns metadata beside sides.
Verified: 178 tests pass (was 157) with both example datasets present;
`assert_unit_consistency=True` passes on real MOM6 and ECCO under the stricter
magnitude check, with ECCO closure residuals unchanged; three notebooks
re-executed; docs build clean under `-W`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
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.
An independent reviewer audited the units feature (both #32 and #33) for bugs and
separately tried to write a recipe for their own model using only the docs. This
acts on what was worth acting on. Targets
units-handling(#32), since #33 wasmerged into it while this was in progress.
Everything here reproduces — each fix landed with the failing case first.
Correctness
The ECCO salt budget's stamped units were wrong by 1000×
ECCO's salt fluxes carry the salinity scale in their own units string
(
ADVx_SLTis1e-3 m3 s-1), and the recipe's compensatingunit_conversion: 0.001was a bare number, hence dimensionless. So the1e-3survived into the composed units while the arithmetic already producedkg/s:
Eight ECCO variables shipped that label. The budgets still closed (both sides
carried the same error), so nothing looked wrong — but the attribute is written
to disk, and it is the thing this feature exists to get right.
The conversion constants now declare the reciprocal scale, so
value × units == 1and the scale cancels:
ECCO salt now stamps
kg.s-1. Values are byte-identical — closure residualsunchanged (mass 2.590e-04, heat 5.111e-06, salt 1.206e-04, exactly as before).
assert_unit_consistencynow checks magnitude, not just dimensionis_convertibleis blind to a numeric scale, so it passed the above. It nowrequires the inferred units to equal the declared ones, and reports a
scale-mismatchnaming the factor and the fix. Both shipped models reconcileexactly under the stricter check.
lateral_divergencewithout a grid raisedAttributeErrorA regression from #33: the rechunking reads
self.grid.axesbefore the guardedhelper is reached.
Both grid-needing operations now share one
_require_grid, and the divergencehas the grid-guard test
differencealready had.show_recipeblanked the units above any missing diagnosticStatic inference had no notion of "the evaluator will drop this operand", so one
absent diagnostic poisoned every ancestor — on the raw ECCO dataset, all three
rhs roots, the most useful nodes in the display:
infer_units(node, lookup, available=)now mirrors the run when told whichdiagnostics exist: a sum drops what the evaluator drops, a product missing a
factor is dropped whole, an
optionalsubtree stops blanking its parent. A termthat genuinely will not be built stays blank — leniency, not optimism.
Smaller fixes
constantsentry with novalue→ bareKeyErrorBudgetParseErrornaming the pathconstants→ sameKeyErrorreciprocaloperand unannotated in the HTML treeUnitErrorpreemptedMissingDiagnosticErrorunits:on a term → "you may be missing an enclosingproduct"Docs
The reviewer got through recipe-writing on the docs alone and was stopped by one
thing:
recipes.mdnever showed how to build anxgcm.Grid. Quickstart saysa grid is needed and to "pass it in exactly the same way", but nothing shows
constructing one — and xgcm ≥ 0.10 renamed
boundary=topadding=and removedperiodic=, so every older tutorial they'd find hands them aValueError. Theyhad to read
tests/conftest.py. There is now a### The gridsection with aworked snippet and a note about the rename.
Also:
dimensionless and stay bare" — is what led them into the 1000× bug. It now
says the opposite, with the worked pattern.
sum/productoperands only, not"a string operand" (a
difference/reciprocaloperand is always a diagnostic).aggregate()returns metadata besidelhs/rhs, and now that every recipedeclares
unitsthat trips anyone iterating the values. Noted in quickstart.Verified
characterization golden runs rather than skips — it still matches.
assert_unit_consistency=Truepasses on real MOM6 and real ECCO V4r4 LLC90under the stricter magnitude check.
-W --keep-going.Reviewer findings deliberately not acted on
_ALIASESis a whole-string map, sopsu m3 s-1parses as unknown whilepsualiases to1. No shipped recipe or example dataset hits it, andtoken-level rewriting of a UDUNITS string is its own small risk. Worth an issue.
kg.s-1, notkg s-1). LegalUDUNITS and consistent, but it does differ from what input datasets write.
A deliberate choice worth confirming rather than a bug.
that a similarly-named constant was declared. The evaluator has no access to
the table by then (references are substituted at parse time), so a good hint
needs plumbing that did not seem worth it here.
🤖 Generated with Claude Code