Skip to content

Fix lon_bnds wraparound for the 0°-crossing cell on global grids - #635

Merged
rbeucher merged 2 commits into
mainfrom
fix-lon-bnds-wraparound-634
Aug 24, 2026
Merged

Fix lon_bnds wraparound for the 0°-crossing cell on global grids#635
rbeucher merged 2 commits into
mainfrom
fix-lon-bnds-wraparound-634

Conversation

@rbeucher

Copy link
Copy Markdown
Member

Summary

  • calculate_longitude_bounds normalized every bound into [0, 360), including the first cell's lower bound on a global grid that straddles the 0°/360° seam. That turned a correct -0.9375 into 359.0625, making the pair non-monotonic and leaving lon=0 outside its own declared bounds.
  • mrsofc/slthick hit this because they're self-contained: load_ressource_data only pulls the variable's own coords from the bundled resource file, dropping the file's already-correct lon_bnds and forcing them to be recalculated by the buggy function.
  • Discovery-driven variables (orog, sftgif, rootd, ...) were unaffected only because their raw input files already carry correct lon_bnds and skip recalculation entirely — not because the calculation itself was right.

The bundled resource files (fx.mrsofc_ACCESS-ESM.nc, fx.slthick_ACCESS-ESM.nc) already contain correct lon_bnds; no data files needed editing.

Fix

In the "ensure continuity at boundaries" step of calculate_longitude_bounds, detect a non-monotonic boundary cell (rather than checking for the already-normalized-away negative/>360 condition) and let it excurse past the nominal range, matching the convention real grid files already use.

Fixes #634

Test plan

  • Added TestCalculateLongitudeBoundsGlobalWrap in tests/unit/test_utilities.py, covering the 0°-crossing cell, the unaffected last cell, and a non-global grid staying clamped to [0, 360].
  • Reproduced the exact bug against the bundled fx.mrsofc_ACCESS-ESM.nc resource file and confirmed the fix restores lon_bnds[0] = [-0.9375, 0.9375].
  • pixi run -e test python -m pytest tests/unit/test_utilities.py tests/unit/test_atmosphere.py tests/unit/test_base.py tests/unit/test_cmip7_qc.py — 428 passed.

calculate_longitude_bounds forced every bound back into [0, 360),
including the first cell's lower bound on a global grid straddling the
0°/360° seam. That flipped a correct negative bound (-0.9375) into
359.0625, making the pair non-monotonic and leaving lon=0 outside its
own declared bounds.

Self-contained fx variables (mrsofc, slthick) hit this because
load_ressource_data only pulls the variable's own coords, dropping the
bundled resource file's already-correct lon_bnds and forcing them to
be recalculated. Discovery-driven variables were unaffected only
because their raw files already carry correct bounds and skip
recalculation entirely.

Fixes #634
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.2%. Comparing base (590f0aa) to head (f112be7).

Additional details and impacted files
@@           Coverage Diff           @@
##            main    #635     +/-   ##
=======================================
+ Coverage   77.0%   77.2%   +0.2%     
=======================================
  Files         40      40             
  Lines       8518    8513      -5     
  Branches    1593    1590      -3     
=======================================
+ Hits        6562    6574     +12     
+ Misses      1624    1610     -14     
+ Partials     332     329      -3     
Flag Coverage Δ
unit 77.2% <100.0%> (+0.2%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The previous test only exercised the first-cell wraparound path; a
grid whose cell centers are offset by half a spacing puts the seam
under the last cell instead, exercising the other branch of the fix.
@rbeucher

Copy link
Copy Markdown
Member Author

@rhaegar325 , Happy with that?

@rbeucher
rbeucher requested a review from rhaegar325 August 24, 2026 03:58
@rbeucher
rbeucher merged commit 3ccc6ca into main Aug 24, 2026
4 checks passed
@rbeucher
rbeucher deleted the fix-lon-bnds-wraparound-634 branch August 24, 2026 10:21
rbeucher pushed a commit that referenced this pull request Aug 24, 2026
…01) (#647)

`areacella` was written with no `lat_bnds`/`lon_bnds` and no `bounds`
attribute on either coordinate, failing two weight-3 WCRP checks:

    [ATTR001] Required variable 'lat' attribute 'bounds' is missing.
    [ATTR001] Required variable 'lon' attribute 'bounds' is missing.

The missing attribute was a symptom: the bounds variables were never
created. `select_and_process_variables` returns early for
`calculation.type == "internal"`, skipping the
`_get_required_bounds_variables` / `calculate_missing_bounds_variables`
pair that every other path reaches. The vocabulary was already correct —
asked directly it reports `must_have_bounds='yes'` for both axes and
`bnds_required = ['lon_bnds', 'lat_bnds']` — the answer just had no
consumer. `reorder()` already reserves slots for `lat_bnds`/`lon_bnds`,
so their absence was the anomaly, not the design.

Separately, `calculate_areacella` builds a full (145, 2) `lat_bnds` array
to derive the cell areas and then discards it, and never computes
`lon_bnds` at all. Rather than return those, call the existing bounds
machinery before the early return: it fixes the class rather than the one
variable, and `calculate_longitude_bounds` already unwraps the 0°-crossing
first cell (#635), so no new bounds arithmetic is introduced.

`areacella` is currently the only internal-calculation entry in the
ESM1-5 and ESM1-6 mappings. `ocean.py` and `sea_ice.py` carry the same
early return, but have no internal variables to exercise it; left alone
rather than changing untestable paths.

Verified end to end: `lat_bnds`/`lon_bnds` are now bit-identical to
`orog`'s on the shared N96 grid (first lon cell `[-0.9375, 0.9375]`, not
the wrapped form), both ATTR001 bounds checks go `[0, 1]` -> `[1, 1]`, and
the two `[VAR012]` bounds-consistency checks — which could not run before —
pass. `orog` output is byte-for-byte unchanged.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

Compliance checker report: Self-contained fx mappings write lon_bnds wrapped at 0°, failing bounds consistency

1 participant