ECMWF_experiments / aifs /era5_worker.py
EmmaScharfmann's picture
EmmaScharfmann HF Staff
add comparision
786225b
Raw
History Blame Contribute Delete
4.41 kB
"""
aifs.era5_worker
================
Standalone subprocess entry point — reads EarthMover's public ERA5
Icechunk/Zarr-v3 store on S3 (anonymous access, no account needed).
This script is deliberately NOT imported by the rest of the app. It is
invoked via ``subprocess`` with ``PYTHONPATH`` pointed at an isolated
``pip install --target`` directory (see :mod:`aifs.era5_env`), because
``icechunk`` requires ``zarr>=3`` while ``anemoi-datasets`` (already a
hard dependency of this Space, for AIFS inference) pins ``zarr<=2.18``.
The two cannot coexist in one interpreter's import path, so this script
only ever runs in a separate process with its own isolated zarr install.
Only stdlib + numpy + icechunk + zarr are imported here — keep it that
way so it never accidentally picks up the main env's (incompatible)
zarr via a transitive import.
Usage
-----
python3 era5_worker.py <request.json> <response_prefix>
``request.json`` is a list of ``{"group", "var", "level", "time_idx"}``
dicts. Writes ``<response_prefix>.npz`` (arrays keyed by request index,
one per successfully-read request) and ``<response_prefix>.meta.json``
(``{"errors": {...}, "resolved_levels": {...}}``). Progress is reported
as ``PROGRESS <i>/<n> <group>/<var>`` lines on stdout.
"""
import json
import sys
import time
import numpy as np
BUCKET = "earthmover-icechunk-era5"
PREFIX = "icechunkV2"
REGION = "us-east-1"
MAX_RETRIES = 6
_RETRIABLE_KEYWORDS = (
"429", "rate limit", "too many requests", "timeout", "connection reset",
"503", "service unavailable", "throughput", "streaming error", "i/o error",
)
def _retriable(exc: Exception) -> bool:
msg = str(exc).lower()
return any(k in msg for k in _RETRIABLE_KEYWORDS)
def _with_retry(fn, label: str):
for attempt in range(MAX_RETRIES):
try:
return fn()
except Exception as exc:
if attempt < MAX_RETRIES - 1 and _retriable(exc):
wait = min(3 * (2 ** attempt), 30)
print(f"RETRY {label}: {exc} (attempt {attempt + 2}/{MAX_RETRIES}, waiting {wait}s)", flush=True)
time.sleep(wait)
else:
raise
def _open_store():
import icechunk
def _open():
storage = icechunk.s3_storage(bucket=BUCKET, prefix=PREFIX, region=REGION, anonymous=True)
repo = icechunk.Repository.open(storage)
session = repo.readonly_session("main")
return session.store
return _with_retry(_open, "open icechunk repo")
def main():
request_path, response_prefix = sys.argv[1], sys.argv[2]
with open(request_path) as f:
requests = json.load(f)
import zarr
store = _open_store()
groups = {}
level_coords = {}
results = {}
errors = {}
resolved_levels = {}
n = len(requests)
for i, req in enumerate(requests):
group, var, level, time_idx = req["group"], req["var"], req.get("level"), req["time_idx"]
print(f"PROGRESS {i + 1}/{n} {group}/{var}" + (f"@{level}hPa" if level is not None else ""), flush=True)
try:
if group not in groups:
groups[group] = _with_retry(
lambda g=group: zarr.open_group(store, mode="r", path=f"{g}/spatial"),
f"open group {group}",
)
g = groups[group]
if var not in g.array_keys():
raise KeyError(f"'{var}' not found in ERA5 group '{group}' (have: {sorted(g.array_keys())})")
arr = g[var]
def _read():
if level is not None:
if group not in level_coords:
level_coords[group] = np.asarray(g["pressure_level"][:])
levels = level_coords[group]
pos = int(np.argmin(np.abs(levels - level)))
resolved_levels[str(i)] = float(levels[pos])
return arr[time_idx, pos]
return arr[time_idx]
data = _with_retry(_read, f"read {group}/{var}")
results[str(i)] = np.asarray(data, dtype=np.float32)
except Exception as exc:
errors[str(i)] = str(exc)
np.savez_compressed(f"{response_prefix}.npz", **results)
with open(f"{response_prefix}.meta.json", "w") as f:
json.dump({"errors": errors, "resolved_levels": resolved_levels}, f)
if __name__ == "__main__":
main()