Fixes a north–south flip of the geopotential cube in the ECMWF/ERA-5 model-level reader. - #811
Conversation
…l reader
_load_model_level reversed only the first axis of z (the level axis) inside
the 'lats are descending' branch, while t, q and lnsp had their latitude axis
reversed. z needs BOTH: the latitude axis to stay aligned with the other
fields, and the level axis because z[0] is subsequently passed to
_calculategeoh as the surface geopotential.
The result was a height field mirrored north-south against the meteorology.
Diagnostics over a 3-degree box in southern California (ERA-5, model levels):
corr(surface height, -surface pressure) before -0.5415 after +0.9992
sea-level ZTD range across the domain before 1621-3233 mm
after 2239-2395 mm
Because the error is antisymmetric in latitude it largely cancels in a domain
mean, which is why it survived: the mean ZTD moved only 2360 -> 2340 mm while
individual grid points were wrong by hundreds of mm. Against UNR GNSS ZTD at
four stations, per-station residuals go from +-400-500 mm to -13 to -66 mm.
Builds a synthetic ERA-5 model-level file in ECMWF's native descending-latitude ordering, with terrain rising from 0 m in the south to 3000 m in the north so a latitude reflection is unambiguous, then loads it through _load_model_level. The two load-bearing assertions: * surface height per latitude row matches the terrain it was built from * corr(surface height, -surface pressure) > 0.99 Both fail on the pre-fix code (the correlation comes out at -0.9986); the ordering assertions pass either way, since they are unaffected by the flip. The domain mean is deliberately not asserted on: reflecting the height field in latitude leaves it essentially unchanged, which is why the original bug went unnoticed. All assertions here are per-column.
|
@jlmaurer Confirmed — this fixes the latitude mirror. I re-ran the reproduction harness from the evidence repo against this branch at its current head 919d6b6, with the same raw ERA-5 ML file and the same frozen dependency set as the Result: the mirror is gone.
The fitted coefficients changed by exactly +1.000000 for I also ran the new Updated results are in the evidence repo, pinned at The remaining ~10 m offset (tracked separately)The remaining ~+9.5–10.2 m is a separate, much smaller correctness issue: after the fix, |
|
Great catch on this and thanks @s-sasaki-earthsea-wizard for the detailed verification! Quick review notes where I distilled everything for easy reference later:
LGTM! I'll approve |
|
Thanks @sssangha ! |
The rebase onto dbekaert#808 left _get_from_cds still issuing one merged request for lnsp/z/t/q, which cannot work: CDS returns a mixed-level request as separate files, and where a single file does come back the surface fields are padded onto the full 137-level axis so .squeeze() no longer yields the (lat, lon) arrays calcgeoh and _makeDataCubes expect. _batch_get_from_cds already splits the request correctly and documents why, so rather than repeat that logic, treat a single date as a one-element batch. Both level types now share one download path, which is what stops the two from drifting apart again -- the pl path was already delegating. Also restores the latitude fix from dbekaert#811 in _load_model_level. It was reverted by resolving the rebase conflict in favour of this branch's copy of ecmwf.py, which predates that fix; test_ecmwf_levels.py caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Description
Thanks to @s-sasaki-earthsea-wizard for catching this bug!
Fixes a north–south flip of the geopotential cube in the ECMWF/ERA-5 model-level reader.
In
ECMWF._load_model_level, the branch that corrects ECMWF's descending-latitude ordering reversed the wrong axis ofz:zneeds both axes reversed: the latitude axis to stay aligned witht/q/lnsp, and the level axis becausez[0]is subsequently passed to_calculategeohas the surface geopotential. Reversing only axis 0 satisfied the second requirement while silently violating the first, leaving the height field mirrored in latitude against the meteorology.The visible symptom is columns whose pressure profile is displaced vertically by however much terrain differs between a latitude and its mirror — e.g. a sea-level node reporting constant pressure from −500 m up to 1900 m, which is ~1900 m of fictitious sea-level-density air worth roughly +490 mm of spurious delay.
Affects ERA-5, ERA-5T and HRES on model levels, which is the default level type for those models. The pressure-level path is untouched.
When this was introduced
Introduced in
f81225b("Update for ERA-5 API changes", #751, merged 2025-09-05, released in 0.6.0) — so the regression window is 0.6.0 onward, not the whole history.The statement
z = z[::-1]is older than the bug and was correct when written. What changed is the dimensionality ofzunderneath it:f81225b_makeDataCubesnp.squeeze(block['z'].values)[0, ...]→ 2D (lat, lon)block['z'].values.squeeze()→ 3D (level, lat, lon)z[::-1]_calculategeoh(z, lnsp)_calculategeoh(z[0, :], lnsp, t, q)The ERA-5 API change meant CDS would only return
zat the surface, so RAiDER had to compute the full geopotential cube itself. That moved the[0, ...]surface-extraction out of_makeDataCubesand into the call site asz[0, :], which made the level reversal newly load-bearing — it is what puts the surface at index 0 — at exactly the moment the latitude reversal silently stopped happening. One statement went from serving one purpose to serving a different one, and reads as deliberate either way.Fixes #806
Why this was not caught earlier
The error is antisymmetric in latitude, so it very nearly cancels in a domain average while being severe at individual points. Over a 3-degree box in southern California the mean ZTD moves only 2360 → 2340 mm, but the spread across the domain collapses from physically impossible to correct:
Physics requires the correlation to be ~+1 (high terrain carries low surface pressure).
Validated against UNR GNSS ZTD at four stations (FGNW, MHMS, WLHG, WORG) on four dates in 2020 and 2025, spanning both dry and high-water-vapour conditions. Per-station residuals go from ±400–500 mm to −13 to −66 mm.
Tests
test/test_ecmwf_levels.pybuilds a synthetic ERA-5 model-level file in ECMWF's native descending-latitude ordering, with terrain rising from 0 m in the south to 3000 m in the north so a reflection is unambiguous, and loads it through the real_load_model_level.zis generated withcalcgeohexactly as RAiDER's own_fetchwrites it.Two assertions are load-bearing and both fail on the pre-fix code (the correlation comes out at −0.9986):
corr(surface height, −surface pressure) > 0.99The domain mean is deliberately not asserted on, for the reason above; every assertion is per-column.
Type of change
Checklist: