Skip to content

Switch ERA5 source to earthmover-public/era5 (full archive)#6

Open
aaronspring wants to merge 10 commits into
CliDyn:mainfrom
aaronspring:feat/era5-full-archive
Open

Switch ERA5 source to earthmover-public/era5 (full archive)#6
aaronspring wants to merge 10 commits into
CliDyn:mainfrom
aaronspring:feat/era5-full-archive

Conversation

@aaronspring

@aaronspring aaronspring commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Swaps the Arraylake data source from earthmover-public/era5-surface-aws to earthmover-public/era5.

Why:

  • Longer record: 1940-01-01 to 2025-12-31 (vs 1975-2024 previously) — 50 more years of hourly reanalysis.
  • Fresher data: the new archive is updated quarterly, instead of being a static snapshot.
  • More variables: 38 surface variables vs 22 previously (adds wind gusts, friction velocity, cloud cover by level, snowfall, several radiation/heat-flux terms, and soil temperature levels 2-4). tp (total precipitation) is now available directly in the store instead of being derived as cp + lsp.

Adaptation required (the new repo restructures the store):

  • Group path changed from "{query_type}" to "single/{query_type}" for surface-level variables.
  • Time coordinate renamed from time to valid_time — normalized back to time right after opening the dataset so the rest of the retrieval pipeline (and cached filenames/output variable names) is unaffected.
  • Three variables were renamed in the store: t2t2m, d2d2m, mslpmsl. Added a zarr_name field on ERA5Variable so our catalog's stable short names (t2, d2, mslp) keep mapping correctly to the new store array names.
  • Removed the now-unnecessary compute_tp = cp + lsp fallback since tp is a native variable in the new store.
  • The new archive compresses several variables with the pcodec codec — added numcodecs[pcodec] as a core dependency, otherwise retrieval fails with codec not available: ''pcodec''. earthmover-public/era5 uses pcodec filter
  • The store declares an lsm land-sea-mask coordinate on every variable but ships it unwritten — reads return 721×1440 of pure NaN. It is dropped from downloads rather than propagated as an empty array that looks like a usable mask.

Why the Python floor moves to 3.11

requires-python said >=3.10, but the project could not be installed on 3.10 at alluv lock fails outright with an unsatisfiable-requirements error:

Because the requested Python version (>=3.10) does not satisfy Python>=3.11
and zarr>=3.0.8,<=3.1.6 depends on Python>=3.11, we can conclude that
zarr>=3.0.8,<=3.1.6 cannot be used. ... your project's requirements are unsatisfiable.

Two dependencies force this:

Dependency Requires-Python Why we need it
zarr>=3.0.0 >=3.11 (3.1.x), >=3.12 (3.2.x) Zarr v3 / Icechunk store access — already a dependency before this PR
numcodecs[pcodec]>=0.16.0 >=3.11 New: earthmover-public/era5 compresses with the pcodec codec

So >=3.10 was already wrong on main (zarr alone made it unsatisfiable); this PR's numcodecs pin makes it wrong for a second, independent reason. Since this PR is what introduces that pin, it's the right place to fix the declared floor rather than leave pyproject.toml advertising an environment nobody can install into. The Programming Language :: Python :: 3.10 classifier, the [tool.mypy] python_version, and black's target-version were all saying 3.10 too, and are synced.

Why 3.11 and not 3.12? >=3.12 also resolves, but 3.11 is the lowest floor that actually works, so it's the least restrictive honest choice. Resolution succeeding isn't sufficient evidence on its own — on 3.11, zarr pins back to 3.1.6, so the pcodec codec (the entire reason numcodecs is pinned) has to register against that older zarr. Verified directly rather than assumed:

$ python3.11 -c "from zarr.registry import get_codec_class; print(get_codec_class('numcodecs.pcodec'))"
zarr 3.1.6 | numcodecs 0.16.5
<class 'zarr.codecs.numcodecs._codecs.PCodec'>

3.11 is genuinely supported, not merely resolvable. The Dockerfile (3.12) and CI are unaffected.

Not yet in scope: the new archive also has pressure-level variables (temperature, wind, humidity, geopotential, etc. at 13 levels from 1000-50 hPa, under a separate pressure/ group). retrieve_era5_data only supports the single/ (surface) group for now — wiring up pressure levels is flagged as follow-up work in the system prompt. AgentConfig.data_group now makes that group reachable by configuration.

Review fixes

Subsequent commits address findings from a review of the initial diff:

  • zarr_name had no test coverage — the three store renames were the most breakage-prone part of this change and only failed at runtime, against the network. Now pinned against the store's 38 array names; mutation-verified (retyping t2m, or dropping d2's mapping, each fails four tests).
  • Cache key omitted the data source — a warm data/ directory would keep serving files fetched from era5-surface-aws forever. Names are now prefixed with the source repo. This matters beyond staleness: old spatial cp/lsp/ssr/ssrd files have dims (latitude, longitude, time) where new ones are (time, latitude, longitude).
  • Empty lsm coordinate dropped from downloads (see above).
  • Stale ERA5T 5-day-lag guard removed. That lag describes the near-real-time ERA5T stream, not this quarterly archive; the guard was both too strict and useless (the real limit is months earlier). Date validation now runs before the credential check, since it needs neither credentials nor network — which also fixes test_future_date_returns_error, red for anyone without ARRAYLAKE_API_KEY.
  • fg10 (wind gust, ≥ 0) no longer uses a diverging colormap; invariant pinned by a test.
  • The "reproduce this download yourself" snippet now actually reproduces it (it selected on valid_time and never renamed).
  • "single/" group prefix moved out of retrieval.py into AgentConfig.data_group, next to the data_source it must travel with.

Test plan

  • Full suite passes without ARRAYLAKE_API_KEY — 206 passed, 5 skipped. (Previously test_future_date_returns_error failed in this configuration, on main too.)
  • Full suite passes with a real key, including live e2e tests — 217 passed, 2 skipped.
  • Verified the new repo's group/dimension/variable structure directly via Arraylake (single/spatial, single/temporal, pressure/spatial; 38 vars, valid_time dim, t2m/d2m/msl renames).
  • Live end-to-end retrieval against earthmover-public/era5, covering:
    • Temporal query (t2) and spatial query (sst, different group path)
    • Renamed variables d2d2m and mslpmsl, output correctly named d2/mslp
    • mslp combined with prime-meridian-crossing longitude bounds (lon range −6° → 10°)
    • Native tp (no longer computed from cp + lsp)
    • Download → cache-hit round trip on the new source-tagged filename
    • Uncovered the missing pcodec codec dependency and the all-NaN lsm array in the process
  • Generated snippet extracted and executed verbatim; output diffed against the tool's own — same dims, same coords, np.allclose(..., equal_nan=True) on values.
  • pcodec codec registration confirmed on Python 3.11 (see above).

🤖 Generated with Claude Code

aaronspring and others added 2 commits July 10, 2026 10:48
Swaps the Arraylake data source to earthmover-public/era5, which extends
coverage from 1975-2024 to 1940-2025 with quarterly updates and adds 16
new surface variables (38 total vs 22). Adapts retrieval.py for the new
repo's structure: group path changed from "{query_type}" to
"single/{query_type}", the time dimension is now "valid_time" (renamed
to "time" on open to keep the rest of the pipeline unchanged), and three
variables were renamed in the store (t2->t2m, d2->d2m, mslp->msl) — a
new zarr_name field on ERA5Variable maps catalog short names to the
store's actual array names so external behavior (filenames, output var
names) stays stable. Also drops the old compute_tp = cp + lsp workaround
since tp is now available directly in the store.

Pressure-level variables (13 levels, 8 vars) exist in the new archive
but are not wired up to retrieve_era5_data yet — noted as follow-up
work in the system prompt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The new archive compresses several variables with the pcodec codec.
Without the pcodec extra, retrieval fails with "codec not available:
''pcodec''". Confirmed via a live end-to-end retrieval smoke test
(temporal + spatial queries, renamed vars t2/d2/mslp, native tp,
prime-meridian crossing) against earthmover-public/era5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread src/eurus/config.py
units="K",
description="Temperature of the second soil layer (7-28 cm depth)",
category="land_surface",
typical_range=(220, 330),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if these make sense. how did you come up with typical_range so far? gut feeling or programmatically?

aaronspring and others added 8 commits July 10, 2026 11:37
The t2->t2m, d2->d2m, mslp->msl renames were the most breakage-prone part
of the era5 source switch and had no test at all: a typo in zarr_name only
surfaced at runtime against the network. Pins the store's 38 array names as
STORE_ARRAY_NAMES and asserts the catalog maps onto them bijectively, plus
direct coverage of get_zarr_name (renames, identity, aliases, unknown
passthrough). Verified both mutations — retyping zarr_name and dropping it
entirely — now fail four tests each.

Also refreshes this file for the new dataset: drops the dead ALL_CATALOG_VARS
list and its now-false "tp is derived" comment (tp is native in the new
store), and fixes test_agent_prompt_lists_all_variables to match the prompt's
table row rather than a bare substring — short names like "sd", "sp" and "ie"
occur inside ordinary prompt prose and made it pass vacuously.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`uv lock` fails outright under requires-python = ">=3.10": zarr>=3.0.8
requires Python >=3.11, and the numcodecs[pcodec] pin added for the new era5
store does too. The declared floor and the 3.10 classifier were advertising
an environment in which the project cannot be installed at all.

Verified >=3.11 resolves, and that the pcodec codec — the reason numcodecs is
pinned — actually registers on 3.11, where zarr pins back to 3.1.6
(get_codec_class('numcodecs.pcodec') returns zarr.codecs.numcodecs._codecs.PCodec).
So 3.11 is genuinely supported, not just resolvable; no need to jump to 3.12.

Also syncs the mypy and black target versions, which still said 3.10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
generate_filename() produced era5_VAR_QTYPE_START_END_REGION.zarr with no
source discriminator, and retrieve_era5_data() returns CACHE HIT on a bare
os.path.exists. Any deployment with a warm data/ directory would keep serving
files fetched from era5-surface-aws after the switch to era5, never
re-downloading. That is not merely stale: old spatial cp/lsp/ssr/ssrd files
have dims (latitude, longitude, time) where new ones are (time, latitude,
longitude), and old tp files hold the computed cp + lsp rather than native tp.

Names are now prefixed with the source repo (earthmover-public-era5_...), so
pre-existing era5_* files can never be mistaken for the new store's output.

Verified end to end: first call downloads, second call hits the cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new store declares an `lsm` land-sea-mask coordinate on every single-level
variable, so xarray attached it to every download and wrote it into the output
zarr — a coordinate that was never there before this source switch.

It is not a usable mask. Reading lsm straight from the store, in both
single/temporal and single/spatial, returns 721x1440 of pure NaN: the array is
declared but never written upstream. Propagating it hands callers an empty
array that looks like a land-sea mask and silently masks everything.

Drops it, so output coords are exactly (time, latitude, longitude) on both the
normal and prime-meridian-concat paths. Also documents the output shape in the
system prompt, since the agent writes python_repl code against these files, and
points at `sst.notnull()` as the mask that actually works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "single/" prefix was hardcoded in two places in retrieval.py while
data_source sat in AgentConfig. The two describe the same store and are only
valid together, so pointing data_source at another repo silently produced
wrong group paths with no error.

Adds AgentConfig.data_group, used by both call sites, with a comment saying
the two fields travel together. Also makes the pressure-level group reachable
by configuration when someone wires it up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wind gust speed was given RdBu_r, copied from u10/v10 where a diverging map is
right because those components are signed. Gusts are >= 0 (its own
typical_range says (0, 60)), so RdBu_r centres white in the middle of the data
and reads as if half the field were negative.

Switches it to viridis, matching zust, the other non-negative wind diagnostic.

Adds a test pinning the invariant: any variable using RdBu_r must have a
typical_range that straddles zero. The six remaining users (u10, v10, u100,
v100, slhf, ie) all do; reverting fg10 fails the test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wnload

The snippet says "Reproduce this download yourself" but selected on valid_time
and never renamed, so pasting it gave you a file whose time dim was
valid_time, not time — and, after the previous commit, one that still carried
the all-NaN lsm coordinate the tool now drops.

Mirrors the two normalisation steps the pipeline performs, with comments
saying why each is needed.

Verified by extracting the generated python block and executing it verbatim,
then diffing against the tool's own output: same dims, same coords, and
np.allclose(equal_nan=True) on the values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…entials

retrieve_era5_data rejected any start_date newer than now-5d, citing an "ERA5T
~5-day processing lag". That lag describes the near-real-time ERA5T stream, not
this archive, which is a reanalysis of the past extended quarterly. The guard
was simultaneously too strict (refusing dates it had no reason to refuse) and
useless (the real limit, currently 2025-12-31, is months earlier).

Replaces it with the only check that cannot go stale — reject dates in the
future — and lets the existing empty-time-range branch report actual coverage,
which it reads from the store and so tracks each quarterly update. That branch
now names both ends of the range rather than only the last timestep.

Moves the check ahead of the API-key lookup: it is pure input validation and
needs no credentials. This also fixes test_future_date_returns_error, which had
been failing for anyone without ARRAYLAKE_API_KEY in their environment — it
asserted on "future" but the function returned the missing-key error first,
never reaching the date logic its docstring claimed it tested.

Also drops the now-unused timedelta import and the same stale ERA5T note from
the all-NaN error message.

Full suite now passes both without a key (206 passed) and with one, including
the live e2e tests (217 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@koldunovn
koldunovn requested a review from dmpantiu July 13, 2026 11:33
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.

1 participant