File size: 5,033 Bytes
d7d41bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
aifs.era5_env
=============
Bootstraps and drives the isolated environment that talks to EarthMover's
ERA5 Icechunk store (see :mod:`aifs.era5_worker`).

Why isolated: ``icechunk`` requires ``zarr>=3``, but ``anemoi-datasets``
(already required for AIFS inference in this Space) pins ``zarr<=2.18``.
pip cannot satisfy both in one environment. Instead we ``pip install
--target=`` a private directory on first use, and run the actual ERA5
reads in a subprocess whose ``PYTHONPATH`` is prepended with that
directory — the subprocess resolves ``zarr`` to the isolated v3 install
regardless of what's importable from the main env's site-packages
(verified: this is plain CPython import-order semantics, not a hack
specific to zarr).
"""

from __future__ import annotations

import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

import numpy as np

_HERE = Path(__file__).resolve().parent
WORKER_SCRIPT = _HERE / "era5_worker.py"
ISOLATED_DIR = _HERE.parent / "era5_env_isolated"
SENTINEL = ISOLATED_DIR / ".bootstrap_ok"

ISOLATED_PACKAGES = ["icechunk>=2.1", "zarr>=3", "pcodec"]


def _fingerprint() -> str:
    """
    Identifies the machine/interpreter the isolated install's compiled
    wheels (numpy, icechunk, pcodec) are built for. Compared against the
    sentinel on every call so a directory built on one machine (e.g. a
    dev sandbox) is never reused on another (e.g. the actual Space host)
    — compiled extensions are platform- and Python-version-specific, and
    a mismatch fails with a confusing "numpy C-extensions" ImportError
    rather than anything that points at the real cause.
    """
    return f"{platform.system()}-{platform.machine()}-py{sys.version_info.major}.{sys.version_info.minor}"


def ensure_bootstrapped(log=lambda msg: None) -> None:
    """Install the isolated zarr>=3 / icechunk stack if not already present for this machine."""
    fingerprint = _fingerprint()
    if SENTINEL.exists() and SENTINEL.read_text().strip() == fingerprint:
        return

    if ISOLATED_DIR.exists():
        log("🔄  Isolated ERA5 environment was built for a different machine — reinstalling…")
        shutil.rmtree(ISOLATED_DIR)

    ISOLATED_DIR.mkdir(parents=True, exist_ok=True)
    log(f"📦  Setting up isolated ERA5 environment (one-time, ~30s)…")
    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", "-q", "--target", str(ISOLATED_DIR), *ISOLATED_PACKAGES],
        capture_output=True, text=True,
    )
    if result.returncode != 0:
        raise RuntimeError(
            f"Failed to bootstrap the isolated ERA5 environment:\n{result.stderr[-2000:]}"
        )
    SENTINEL.write_text(fingerprint)
    log("✅  Isolated ERA5 environment ready.")


def _subprocess_env() -> dict:
    env = dict(os.environ)
    existing = env.get("PYTHONPATH", "")
    env["PYTHONPATH"] = f"{ISOLATED_DIR}{os.pathsep}{existing}" if existing else str(ISOLATED_DIR)
    return env


def fetch_era5_fields(requests: list[dict], log=lambda msg: None, timeout: int = 600) -> tuple[dict, dict]:
    """
    Run a batch of ERA5 reads in the isolated subprocess.

    ``requests`` — list of ``{"group", "var", "level", "time_idx"}`` dicts.
    Returns ``(arrays, meta)`` where ``arrays`` maps request index (int) to
    a ``(721, 1440)`` float32 grid, and ``meta`` has ``"errors"`` (index ->
    message, for requests that failed) and ``"resolved_levels"``.
    """
    ensure_bootstrapped(log)

    with tempfile.TemporaryDirectory() as tmp:
        request_path = Path(tmp) / "request.json"
        response_prefix = Path(tmp) / "response"
        request_path.write_text(json.dumps(requests))

        proc = subprocess.Popen(
            [sys.executable, str(WORKER_SCRIPT), str(request_path), str(response_prefix)],
            env=_subprocess_env(),
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1,
        )
        stderr_tail = []
        try:
            for line in proc.stdout:
                line = line.rstrip()
                stderr_tail.append(line)
                if line.startswith("PROGRESS") or line.startswith("RETRY"):
                    log(f"📡  {line}")
            proc.wait(timeout=timeout)
        except subprocess.TimeoutExpired:
            proc.kill()
            raise RuntimeError("Timed out waiting for the ERA5 worker subprocess.")

        if proc.returncode != 0:
            raise RuntimeError(
                "ERA5 worker subprocess failed:\n" + "\n".join(stderr_tail[-30:])
            )

        npz_path = f"{response_prefix}.npz"
        meta_path = f"{response_prefix}.meta.json"
        with np.load(npz_path) as npz:
            arrays = {int(k): npz[k] for k in npz.files}
        meta = json.loads(Path(meta_path).read_text())
        meta["errors"] = {int(k): v for k, v in meta["errors"].items()}
        meta["resolved_levels"] = {int(k): v for k, v in meta["resolved_levels"].items()}
        return arrays, meta