Emma Scharfmann commited on
Commit
889b369
·
1 Parent(s): 6150ea0

see torch and cuda config

Browse files
Files changed (4) hide show
  1. README.md +1 -1
  2. app-test.py +348 -0
  3. app.py +15 -344
  4. app2.py +0 -19
README.md CHANGED
@@ -6,7 +6,7 @@ colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.19.0
8
  python_version: '3.12'
9
- app_file: app2.py
10
  pinned: false
11
  license: apache-2.0
12
  tags:
 
6
  sdk: gradio
7
  sdk_version: 6.19.0
8
  python_version: '3.12'
9
+ app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
  tags:
app-test.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AIFS Single v2 — Gradio Forecast App
3
+ Runs ECMWF AIFS Single v2 inference and displays output fields interactively.
4
+
5
+ Requirements (install in Colab with an L4 or A100 runtime):
6
+ pip install gradio
7
+ pip install anemoi-inference[huggingface]==0.8.3 anemoi-models==0.9.3 anemoi-utils==0.4.35.post3
8
+ pip install torch==2.7.0 torch-geometric==2.6.1
9
+ pip install earthkit-regrid==0.5.1 ecmwf-opendata==0.3.29 'earthkit-data<1.0.0'
10
+ pip install flash-attn==2.7.4.post1
11
+ pip install matplotlib cartopy
12
+ """
13
+
14
+ import os
15
+ import datetime
16
+ from collections import defaultdict
17
+
18
+ import numpy as np
19
+ import gradio as gr
20
+ import spaces
21
+
22
+
23
+ # ── Lazy imports so Gradio loads even before heavy deps are installed ──────────
24
+
25
+ def _import_deps():
26
+ import torch
27
+ import earthkit.data as ekd
28
+ import earthkit.regrid as ekr
29
+ from anemoi.inference.runners.simple import SimpleRunner
30
+ from ecmwf.opendata import Client as OpendataClient
31
+ import matplotlib
32
+ matplotlib.use("Agg")
33
+ import matplotlib.pyplot as plt
34
+ import cartopy.crs as ccrs
35
+ import cartopy.feature as cfeature
36
+ import matplotlib.tri as tri
37
+ return torch, ekd, ekr, SimpleRunner, OpendataClient, plt, ccrs, cfeature, tri
38
+
39
+
40
+ # ── Constants ─────────────────────────────────────────────────────────────────
41
+
42
+ PARAM_SFC = ["10u", "10v", "2d", "2t", "msl", "skt", "sp", "tcw", "lsm", "z", "slor", "sdor", "sd"]
43
+ PARAM_SOIL = ["vsw", "sot"]
44
+ PARAM_WAVE = ["wmb", "h1012", "h1214", "h1417", "h1721", "h2125", "h2530", "mwd", "cdww", "mwp", "swh"]
45
+ PARAM_PL = ["gh", "t", "u", "v", "q"]
46
+ LEVELS = [1000, 925, 850, 700, 600, 500, 400, 300, 250, 200, 150, 100, 50, 10]
47
+ SOIL_LEVELS = [1, 2]
48
+
49
+ CHECKPOINT = {"huggingface": "ecmwf/aifs-single-2.0"}
50
+
51
+ PLOTTABLE_FIELDS = [
52
+ "100u", "100v", "10u", "10v", "2t", "msl", "sp", "tcw", "swh", "mwp",
53
+ "t_850", "t_500", "u_850", "v_850", "z_500", "q_700",
54
+ ]
55
+
56
+ CMAP_MAP = {
57
+ "2t": "RdBu_r", "t_850": "RdBu_r", "t_500": "RdBu_r",
58
+ "msl": "viridis", "sp": "viridis",
59
+ "100u": "RdBu", "100v": "RdBu", "10u": "RdBu", "10v": "RdBu",
60
+ "u_850": "RdBu", "v_850": "RdBu",
61
+ "swh": "Blues", "mwp": "Blues", "tcw": "Blues",
62
+ "z_500": "plasma", "q_700": "YlGn",
63
+ }
64
+
65
+ UNITS_MAP = {
66
+ "2t": "K", "t_850": "K", "t_500": "K",
67
+ "msl": "Pa", "sp": "Pa", "z_500": "m²/s²",
68
+ "100u": "m/s", "100v": "m/s", "10u": "m/s", "10v": "m/s",
69
+ "u_850": "m/s", "v_850": "m/s",
70
+ "swh": "m", "mwp": "s", "tcw": "kg/m²", "q_700": "kg/kg",
71
+ }
72
+
73
+ SOURCE = "ecmwf"
74
+
75
+ # ── Global state (populated during the run) ───────────────────────────────────
76
+
77
+ _runner = None # cached runner
78
+ _states = [] # list of forecast state dicts
79
+
80
+
81
+ # ── Helper: download + interpolate ────────────────────────────────────────────
82
+
83
+ def _get_open_data(ekd, ekr, date, param, levelist=[], **kwargs):
84
+ fields = defaultdict(list)
85
+ for d in [date - datetime.timedelta(hours=6), date]:
86
+ data = ekd.from_source(
87
+ "ecmwf-open-data", date=d, param=param,
88
+ levelist=levelist, source=SOURCE, **kwargs
89
+ )
90
+ for f in data:
91
+ assert f.to_numpy().shape == (721, 1440)
92
+ values = np.roll(f.to_numpy(), -f.shape[1] // 2, axis=1)
93
+ values = ekr.interpolate(values, {"grid": (0.25, 0.25)}, {"grid": "N320"})
94
+ name = (
95
+ f"{f.metadata('param')}_{f.metadata('levelist')}"
96
+ if levelist else f.metadata("param")
97
+ )
98
+ fields[name].append(values)
99
+ for k, v in fields.items():
100
+ fields[k] = np.stack(v)
101
+ return fields
102
+
103
+
104
+ # ── Core inference function ───────────────────────────────────────────────────
105
+
106
+ @spaces.GPU
107
+ def run_forecast(lead_time: int, num_chunks: int, progress=gr.Progress(track_tqdm=True)):
108
+ """Download ICs, run AIFS, return status message."""
109
+ global _runner, _states
110
+
111
+ torch, ekd, ekr, SimpleRunner, OpendataClient, plt, ccrs, cfeature, tri = _import_deps()
112
+
113
+ # GPU check
114
+ if not torch.cuda.is_available():
115
+ return "❌ No CUDA GPU detected. This model requires an Ampere GPU (e.g. Colab L4/A100).", gr.update(choices=[])
116
+
117
+ major, _ = torch.cuda.get_device_capability(0)
118
+ if major < 8:
119
+ gpu = torch.cuda.get_device_name(0)
120
+ return f"❌ GPU '{gpu}' is below Ampere (compute capability < 8.0). FlashAttention requires Ampere or newer.", gr.update(choices=[])
121
+
122
+ # Memory optimisation
123
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
124
+ os.environ["ANEMOI_INFERENCE_NUM_CHUNKS"] = str(num_chunks)
125
+
126
+ yield "⬇️ Fetching latest ECMWF open-data date…", gr.update(choices=[])
127
+
128
+ ekd.config.set({"cache-policy": "user"})
129
+ DATE = OpendataClient(SOURCE).latest()
130
+
131
+ yield f"📅 Initial date: {DATE} | ⬇️ Downloading surface fields…", gr.update(choices=[])
132
+ fields = {}
133
+ fields.update(_get_open_data(ekd, ekr, DATE, PARAM_SFC, levtype="sfc"))
134
+
135
+ yield "⬇️ Downloading wave fields…", gr.update(choices=[])
136
+ fields.update(_get_open_data(ekd, ekr, DATE, PARAM_WAVE, stream="wave"))
137
+
138
+ yield "⬇️ Downloading soil fields…", gr.update(choices=[])
139
+ soil = _get_open_data(ekd, ekr, DATE, PARAM_SOIL, levelist=SOIL_LEVELS)
140
+
141
+ yield "⬇️ Downloading pressure-level fields…", gr.update(choices=[])
142
+ fields.update(_get_open_data(ekd, ekr, DATE, PARAM_PL, levelist=LEVELS))
143
+
144
+ # Transforms
145
+ yield "🔧 Applying data transformations…", gr.update(choices=[])
146
+ mwd = fields.pop("mwd")
147
+ mwd_rad = np.deg2rad(mwd)
148
+ fields["cos_mwd"] = np.cos(mwd_rad)
149
+ fields["sin_mwd"] = np.sin(mwd_rad)
150
+
151
+ mapping = {"sot_1": "stl1", "sot_2": "stl2", "vsw_1": "swvl1", "vsw_2": "swvl2"}
152
+ for k, v in soil.items():
153
+ fields[mapping[k]] = v
154
+
155
+ fields.pop("q_10", None)
156
+ fields.pop("q_50", None)
157
+
158
+ try:
159
+ mask = np.equal(ekd.from_source("file", "lsm.grib")[0].to_numpy(flatten=True), 0)
160
+ for var in ("sd", "swvl1", "swvl2"):
161
+ fields[var][:, mask] = np.nan
162
+ except Exception:
163
+ pass # lsm.grib optional
164
+
165
+ for level in LEVELS:
166
+ gh = fields.pop(f"gh_{level}")
167
+ fields[f"z_{level}"] = gh * 9.80665
168
+
169
+ input_state = dict(date=DATE, fields=fields)
170
+
171
+ # Load runner (cache it)
172
+ yield "🤖 Loading AIFS Single v2 checkpoint from Hugging Face…", gr.update(choices=[])
173
+ if _runner is None:
174
+ _runner = SimpleRunner(CHECKPOINT)
175
+
176
+ # Run inference
177
+ _states = []
178
+ yield f"🌍 Running {lead_time}h forecast…", gr.update(choices=[])
179
+ for state in _runner.run(input_state=input_state, lead_time=lead_time):
180
+ _states.append(state)
181
+
182
+ timestamps = [str(s["date"]) for s in _states]
183
+ available = [f for f in PLOTTABLE_FIELDS if f in _states[-1]["fields"]]
184
+
185
+ yield (
186
+ f"✅ Forecast complete! {len(_states)} time steps generated "
187
+ f"({lead_time}h at 6h intervals). GPU: {torch.cuda.get_device_name(0)}",
188
+ gr.update(choices=timestamps, value=timestamps[-1]),
189
+ )
190
+
191
+
192
+ # ── Plot function ─────────────────────────────────────────────────────────────
193
+
194
+ def plot_field(field_name: str, timestamp: str):
195
+ if not _states:
196
+ return None, "Run the forecast first."
197
+
198
+ torch, *rest = _import_deps()
199
+ plt = rest[4]; ccrs = rest[5]; cfeature = rest[6]; tri_mod = rest[7]
200
+
201
+ state = next((s for s in _states if str(s["date"]) == timestamp), _states[-1])
202
+
203
+ if field_name not in state["fields"]:
204
+ return None, f"Field '{field_name}' not available in this forecast state."
205
+
206
+ lats = state["latitudes"]
207
+ lons = state["longitudes"]
208
+ values = state["fields"][field_name]
209
+
210
+ # Shift lons 0-360 → -180-180
211
+ lons_shifted = np.where(lons > 180, lons - 360, lons)
212
+
213
+ cmap = CMAP_MAP.get(field_name, "viridis")
214
+ units = UNITS_MAP.get(field_name, "")
215
+
216
+ fig, ax = plt.subplots(
217
+ figsize=(12, 6),
218
+ subplot_kw={"projection": ccrs.PlateCarree()},
219
+ facecolor="#0d1117",
220
+ )
221
+ ax.set_facecolor("#0d1117")
222
+ ax.coastlines(color="#8b9ab0", linewidth=0.7)
223
+ ax.add_feature(cfeature.BORDERS, linestyle=":", edgecolor="#5a6a7e", linewidth=0.5)
224
+ ax.add_feature(cfeature.OCEAN, facecolor="#111827")
225
+ ax.add_feature(cfeature.LAND, facecolor="#1a2332")
226
+
227
+ triang = tri_mod.Triangulation(lons_shifted, lats)
228
+ cf = ax.tricontourf(
229
+ triang, values, levels=24,
230
+ transform=ccrs.PlateCarree(), cmap=cmap,
231
+ alpha=0.92,
232
+ )
233
+ cbar = fig.colorbar(cf, ax=ax, orientation="vertical", shrink=0.75, pad=0.02)
234
+ cbar.set_label(f"{field_name} [{units}]" if units else field_name,
235
+ color="white", fontsize=10)
236
+ cbar.ax.yaxis.set_tick_params(color="white")
237
+ plt.setp(cbar.ax.yaxis.get_ticklabels(), color="white")
238
+
239
+ title = f"{field_name} | {timestamp}"
240
+ ax.set_title(title, color="white", fontsize=12, pad=10)
241
+
242
+ fig.patch.set_facecolor("#0d1117")
243
+ plt.tight_layout()
244
+
245
+ path = "/tmp/aifs_plot.png"
246
+ fig.savefig(path, dpi=130, bbox_inches="tight", facecolor=fig.get_facecolor())
247
+ plt.close(fig)
248
+
249
+ stats = (
250
+ f"**{field_name}** at {timestamp}\n\n"
251
+ f"- Min: `{values.min():.4g}` {units}\n"
252
+ f"- Max: `{values.max():.4g}` {units}\n"
253
+ f"- Mean: `{values.mean():.4g}` {units}\n"
254
+ f"- Grid points: `{len(values):,}`"
255
+ )
256
+ return path, stats
257
+
258
+
259
+ # ── UI ────────────────────────────────────────────��───────────────────────────
260
+
261
+ DARK_CSS = """
262
+ body, .gradio-container {
263
+ background: #0d1117 !important;
264
+ color: #cdd9e5 !important;
265
+ font-family: 'Inter', 'Segoe UI', sans-serif;
266
+ }
267
+ h1 { color: #58a6ff !important; letter-spacing: -0.5px; }
268
+ h3 { color: #79c0ff !important; }
269
+ .panel { background: #161b22 !important; border: 1px solid #30363d !important; border-radius: 8px; }
270
+ button.primary { background: #1f6feb !important; border: none !important; color: white !important; }
271
+ button.primary:hover { background: #388bfd !important; }
272
+ .label-wrap { color: #8b949e !important; }
273
+ textarea, input, select { background: #1c2128 !important; color: #cdd9e5 !important; border-color: #30363d !important; }
274
+ .output-markdown { color: #cdd9e5 !important; }
275
+ footer { display: none !important; }
276
+ """
277
+
278
+ with gr.Blocks(css=DARK_CSS, title="AIFS Single v2 Forecast") as demo:
279
+ gr.Markdown(
280
+ """
281
+ # 🌍 AIFS Single v2 — Weather Forecast
282
+ **ECMWF's AI Integrated Forecasting System** | Requires Ampere GPU (Colab L4 / A100)
283
+ """
284
+ )
285
+
286
+ with gr.Row():
287
+ with gr.Column(scale=1, elem_classes="panel"):
288
+ gr.Markdown("### ⚙️ Forecast Settings")
289
+ lead_time_sl = gr.Slider(
290
+ minimum=6, maximum=240, step=6, value=24,
291
+ label="Lead time (hours)",
292
+ info="Number of forecast hours. Each 6h step adds ~30–60s on an A100.",
293
+ )
294
+ num_chunks_sl = gr.Slider(
295
+ minimum=1, maximum=32, step=1, value=16,
296
+ label="Memory chunks (ANEMOI_INFERENCE_NUM_CHUNKS)",
297
+ info="Higher = less GPU memory, slightly slower. 16 works well on A100.",
298
+ )
299
+ run_btn = gr.Button("▶ Run Forecast", variant="primary", size="lg")
300
+ status_box = gr.Textbox(
301
+ label="Status", lines=3, interactive=False,
302
+ placeholder="Click 'Run Forecast' to begin…",
303
+ )
304
+
305
+ with gr.Column(scale=2, elem_classes="panel"):
306
+ gr.Markdown("### 🗺️ Visualise Output")
307
+ with gr.Row():
308
+ field_dd = gr.Dropdown(
309
+ choices=PLOTTABLE_FIELDS,
310
+ value="2t",
311
+ label="Field",
312
+ info="Select the variable to plot.",
313
+ )
314
+ timestamp_dd = gr.Dropdown(
315
+ choices=[],
316
+ label="Forecast step",
317
+ info="Available after running the forecast.",
318
+ )
319
+ plot_btn = gr.Button("🖼 Plot Field", variant="secondary")
320
+ map_img = gr.Image(label="Global Map", type="filepath")
321
+ stats_md = gr.Markdown()
322
+
323
+ # Wire up
324
+ run_btn.click(
325
+ fn=run_forecast,
326
+ inputs=[lead_time_sl, num_chunks_sl],
327
+ outputs=[status_box, timestamp_dd],
328
+ )
329
+
330
+ plot_btn.click(
331
+ fn=plot_field,
332
+ inputs=[field_dd, timestamp_dd],
333
+ outputs=[map_img, stats_md],
334
+ )
335
+
336
+ gr.Markdown(
337
+ """
338
+ ---
339
+ **Notes**
340
+ - First run downloads the ~2 GB model checkpoint from Hugging Face and caches it.
341
+ - Data is downloaded from the ECMWF Open Data API (CC BY 4.0 — please attribute ECMWF).
342
+ - Results may differ slightly from operational AIFS due to GPU non-determinism and regrid differences.
343
+ """
344
+ )
345
+
346
+
347
+ if __name__ == "__main__":
348
+ demo.launch(share=True)
app.py CHANGED
@@ -1,348 +1,19 @@
1
- """
2
- AIFS Single v2 — Gradio Forecast App
3
- Runs ECMWF AIFS Single v2 inference and displays output fields interactively.
4
-
5
- Requirements (install in Colab with an L4 or A100 runtime):
6
- pip install gradio
7
- pip install anemoi-inference[huggingface]==0.8.3 anemoi-models==0.9.3 anemoi-utils==0.4.35.post3
8
- pip install torch==2.7.0 torch-geometric==2.6.1
9
- pip install earthkit-regrid==0.5.1 ecmwf-opendata==0.3.29 'earthkit-data<1.0.0'
10
- pip install flash-attn==2.7.4.post1
11
- pip install matplotlib cartopy
12
- """
13
-
14
- import os
15
- import datetime
16
- from collections import defaultdict
17
-
18
- import numpy as np
19
  import gradio as gr
20
- import spaces
21
-
22
-
23
- # ── Lazy imports so Gradio loads even before heavy deps are installed ──────────
24
-
25
- def _import_deps():
26
- import torch
27
- import earthkit.data as ekd
28
- import earthkit.regrid as ekr
29
- from anemoi.inference.runners.simple import SimpleRunner
30
- from ecmwf.opendata import Client as OpendataClient
31
- import matplotlib
32
- matplotlib.use("Agg")
33
- import matplotlib.pyplot as plt
34
- import cartopy.crs as ccrs
35
- import cartopy.feature as cfeature
36
- import matplotlib.tri as tri
37
- return torch, ekd, ekr, SimpleRunner, OpendataClient, plt, ccrs, cfeature, tri
38
-
39
-
40
- # ── Constants ─────────────────────────────────────────────────────────────────
41
-
42
- PARAM_SFC = ["10u", "10v", "2d", "2t", "msl", "skt", "sp", "tcw", "lsm", "z", "slor", "sdor", "sd"]
43
- PARAM_SOIL = ["vsw", "sot"]
44
- PARAM_WAVE = ["wmb", "h1012", "h1214", "h1417", "h1721", "h2125", "h2530", "mwd", "cdww", "mwp", "swh"]
45
- PARAM_PL = ["gh", "t", "u", "v", "q"]
46
- LEVELS = [1000, 925, 850, 700, 600, 500, 400, 300, 250, 200, 150, 100, 50, 10]
47
- SOIL_LEVELS = [1, 2]
48
-
49
- CHECKPOINT = {"huggingface": "ecmwf/aifs-single-2.0"}
50
-
51
- PLOTTABLE_FIELDS = [
52
- "100u", "100v", "10u", "10v", "2t", "msl", "sp", "tcw", "swh", "mwp",
53
- "t_850", "t_500", "u_850", "v_850", "z_500", "q_700",
54
- ]
55
-
56
- CMAP_MAP = {
57
- "2t": "RdBu_r", "t_850": "RdBu_r", "t_500": "RdBu_r",
58
- "msl": "viridis", "sp": "viridis",
59
- "100u": "RdBu", "100v": "RdBu", "10u": "RdBu", "10v": "RdBu",
60
- "u_850": "RdBu", "v_850": "RdBu",
61
- "swh": "Blues", "mwp": "Blues", "tcw": "Blues",
62
- "z_500": "plasma", "q_700": "YlGn",
63
- }
64
-
65
- UNITS_MAP = {
66
- "2t": "K", "t_850": "K", "t_500": "K",
67
- "msl": "Pa", "sp": "Pa", "z_500": "m²/s²",
68
- "100u": "m/s", "100v": "m/s", "10u": "m/s", "10v": "m/s",
69
- "u_850": "m/s", "v_850": "m/s",
70
- "swh": "m", "mwp": "s", "tcw": "kg/m²", "q_700": "kg/kg",
71
- }
72
-
73
- SOURCE = "ecmwf"
74
-
75
- # ── Global state (populated during the run) ───────────────────────────────────
76
-
77
- _runner = None # cached runner
78
- _states = [] # list of forecast state dicts
79
-
80
-
81
- # ── Helper: download + interpolate ────────────────────────────────────────────
82
-
83
- def _get_open_data(ekd, ekr, date, param, levelist=[], **kwargs):
84
- fields = defaultdict(list)
85
- for d in [date - datetime.timedelta(hours=6), date]:
86
- data = ekd.from_source(
87
- "ecmwf-open-data", date=d, param=param,
88
- levelist=levelist, source=SOURCE, **kwargs
89
- )
90
- for f in data:
91
- assert f.to_numpy().shape == (721, 1440)
92
- values = np.roll(f.to_numpy(), -f.shape[1] // 2, axis=1)
93
- values = ekr.interpolate(values, {"grid": (0.25, 0.25)}, {"grid": "N320"})
94
- name = (
95
- f"{f.metadata('param')}_{f.metadata('levelist')}"
96
- if levelist else f.metadata("param")
97
- )
98
- fields[name].append(values)
99
- for k, v in fields.items():
100
- fields[k] = np.stack(v)
101
- return fields
102
-
103
-
104
- # ── Core inference function ───────────────────────────────────────────────────
105
-
106
- @spaces.GPU
107
- def run_forecast(lead_time: int, num_chunks: int, progress=gr.Progress(track_tqdm=True)):
108
- """Download ICs, run AIFS, return status message."""
109
- global _runner, _states
110
-
111
- torch, ekd, ekr, SimpleRunner, OpendataClient, plt, ccrs, cfeature, tri = _import_deps()
112
-
113
- # GPU check
114
- if not torch.cuda.is_available():
115
- return "❌ No CUDA GPU detected. This model requires an Ampere GPU (e.g. Colab L4/A100).", gr.update(choices=[])
116
-
117
- major, _ = torch.cuda.get_device_capability(0)
118
- if major < 8:
119
- gpu = torch.cuda.get_device_name(0)
120
- return f"❌ GPU '{gpu}' is below Ampere (compute capability < 8.0). FlashAttention requires Ampere or newer.", gr.update(choices=[])
121
-
122
- # Memory optimisation
123
- os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
124
- os.environ["ANEMOI_INFERENCE_NUM_CHUNKS"] = str(num_chunks)
125
-
126
- yield "⬇️ Fetching latest ECMWF open-data date…", gr.update(choices=[])
127
-
128
- ekd.config.set({"cache-policy": "user"})
129
- DATE = OpendataClient(SOURCE).latest()
130
-
131
- yield f"📅 Initial date: {DATE} | ⬇️ Downloading surface fields…", gr.update(choices=[])
132
- fields = {}
133
- fields.update(_get_open_data(ekd, ekr, DATE, PARAM_SFC, levtype="sfc"))
134
-
135
- yield "⬇️ Downloading wave fields…", gr.update(choices=[])
136
- fields.update(_get_open_data(ekd, ekr, DATE, PARAM_WAVE, stream="wave"))
137
-
138
- yield "⬇️ Downloading soil fields…", gr.update(choices=[])
139
- soil = _get_open_data(ekd, ekr, DATE, PARAM_SOIL, levelist=SOIL_LEVELS)
140
-
141
- yield "⬇️ Downloading pressure-level fields…", gr.update(choices=[])
142
- fields.update(_get_open_data(ekd, ekr, DATE, PARAM_PL, levelist=LEVELS))
143
-
144
- # Transforms
145
- yield "🔧 Applying data transformations…", gr.update(choices=[])
146
- mwd = fields.pop("mwd")
147
- mwd_rad = np.deg2rad(mwd)
148
- fields["cos_mwd"] = np.cos(mwd_rad)
149
- fields["sin_mwd"] = np.sin(mwd_rad)
150
-
151
- mapping = {"sot_1": "stl1", "sot_2": "stl2", "vsw_1": "swvl1", "vsw_2": "swvl2"}
152
- for k, v in soil.items():
153
- fields[mapping[k]] = v
154
-
155
- fields.pop("q_10", None)
156
- fields.pop("q_50", None)
157
-
158
- try:
159
- mask = np.equal(ekd.from_source("file", "lsm.grib")[0].to_numpy(flatten=True), 0)
160
- for var in ("sd", "swvl1", "swvl2"):
161
- fields[var][:, mask] = np.nan
162
- except Exception:
163
- pass # lsm.grib optional
164
-
165
- for level in LEVELS:
166
- gh = fields.pop(f"gh_{level}")
167
- fields[f"z_{level}"] = gh * 9.80665
168
-
169
- input_state = dict(date=DATE, fields=fields)
170
-
171
- # Load runner (cache it)
172
- yield "🤖 Loading AIFS Single v2 checkpoint from Hugging Face…", gr.update(choices=[])
173
- if _runner is None:
174
- _runner = SimpleRunner(CHECKPOINT)
175
-
176
- # Run inference
177
- _states = []
178
- yield f"🌍 Running {lead_time}h forecast…", gr.update(choices=[])
179
- for state in _runner.run(input_state=input_state, lead_time=lead_time):
180
- _states.append(state)
181
-
182
- timestamps = [str(s["date"]) for s in _states]
183
- available = [f for f in PLOTTABLE_FIELDS if f in _states[-1]["fields"]]
184
-
185
- yield (
186
- f"✅ Forecast complete! {len(_states)} time steps generated "
187
- f"({lead_time}h at 6h intervals). GPU: {torch.cuda.get_device_name(0)}",
188
- gr.update(choices=timestamps, value=timestamps[-1]),
189
- )
190
-
191
-
192
- # ── Plot function ─────────────────────────────────────────────────────────────
193
-
194
- def plot_field(field_name: str, timestamp: str):
195
- if not _states:
196
- return None, "Run the forecast first."
197
-
198
- torch, *rest = _import_deps()
199
- plt = rest[4]; ccrs = rest[5]; cfeature = rest[6]; tri_mod = rest[7]
200
-
201
- state = next((s for s in _states if str(s["date"]) == timestamp), _states[-1])
202
-
203
- if field_name not in state["fields"]:
204
- return None, f"Field '{field_name}' not available in this forecast state."
205
-
206
- lats = state["latitudes"]
207
- lons = state["longitudes"]
208
- values = state["fields"][field_name]
209
-
210
- # Shift lons 0-360 → -180-180
211
- lons_shifted = np.where(lons > 180, lons - 360, lons)
212
-
213
- cmap = CMAP_MAP.get(field_name, "viridis")
214
- units = UNITS_MAP.get(field_name, "")
215
-
216
- fig, ax = plt.subplots(
217
- figsize=(12, 6),
218
- subplot_kw={"projection": ccrs.PlateCarree()},
219
- facecolor="#0d1117",
220
- )
221
- ax.set_facecolor("#0d1117")
222
- ax.coastlines(color="#8b9ab0", linewidth=0.7)
223
- ax.add_feature(cfeature.BORDERS, linestyle=":", edgecolor="#5a6a7e", linewidth=0.5)
224
- ax.add_feature(cfeature.OCEAN, facecolor="#111827")
225
- ax.add_feature(cfeature.LAND, facecolor="#1a2332")
226
-
227
- triang = tri_mod.Triangulation(lons_shifted, lats)
228
- cf = ax.tricontourf(
229
- triang, values, levels=24,
230
- transform=ccrs.PlateCarree(), cmap=cmap,
231
- alpha=0.92,
232
- )
233
- cbar = fig.colorbar(cf, ax=ax, orientation="vertical", shrink=0.75, pad=0.02)
234
- cbar.set_label(f"{field_name} [{units}]" if units else field_name,
235
- color="white", fontsize=10)
236
- cbar.ax.yaxis.set_tick_params(color="white")
237
- plt.setp(cbar.ax.yaxis.get_ticklabels(), color="white")
238
-
239
- title = f"{field_name} | {timestamp}"
240
- ax.set_title(title, color="white", fontsize=12, pad=10)
241
-
242
- fig.patch.set_facecolor("#0d1117")
243
- plt.tight_layout()
244
-
245
- path = "/tmp/aifs_plot.png"
246
- fig.savefig(path, dpi=130, bbox_inches="tight", facecolor=fig.get_facecolor())
247
- plt.close(fig)
248
-
249
- stats = (
250
- f"**{field_name}** at {timestamp}\n\n"
251
- f"- Min: `{values.min():.4g}` {units}\n"
252
- f"- Max: `{values.max():.4g}` {units}\n"
253
- f"- Mean: `{values.mean():.4g}` {units}\n"
254
- f"- Grid points: `{len(values):,}`"
255
- )
256
- return path, stats
257
-
258
-
259
- # ── UI ────────────────────────────────────────────���───────────────────────────
260
-
261
- DARK_CSS = """
262
- body, .gradio-container {
263
- background: #0d1117 !important;
264
- color: #cdd9e5 !important;
265
- font-family: 'Inter', 'Segoe UI', sans-serif;
266
- }
267
- h1 { color: #58a6ff !important; letter-spacing: -0.5px; }
268
- h3 { color: #79c0ff !important; }
269
- .panel { background: #161b22 !important; border: 1px solid #30363d !important; border-radius: 8px; }
270
- button.primary { background: #1f6feb !important; border: none !important; color: white !important; }
271
- button.primary:hover { background: #388bfd !important; }
272
- .label-wrap { color: #8b949e !important; }
273
- textarea, input, select { background: #1c2128 !important; color: #cdd9e5 !important; border-color: #30363d !important; }
274
- .output-markdown { color: #cdd9e5 !important; }
275
- footer { display: none !important; }
276
  """
277
 
278
- with gr.Blocks(css=DARK_CSS, title="AIFS Single v2 Forecast") as demo:
279
- gr.Markdown(
280
- """
281
- # 🌍 AIFS Single v2 — Weather Forecast
282
- **ECMWF's AI Integrated Forecasting System** | Requires Ampere GPU (Colab L4 / A100)
283
- """
284
- )
285
-
286
- with gr.Row():
287
- with gr.Column(scale=1, elem_classes="panel"):
288
- gr.Markdown("### ⚙️ Forecast Settings")
289
- lead_time_sl = gr.Slider(
290
- minimum=6, maximum=240, step=6, value=24,
291
- label="Lead time (hours)",
292
- info="Number of forecast hours. Each 6h step adds ~30–60s on an A100.",
293
- )
294
- num_chunks_sl = gr.Slider(
295
- minimum=1, maximum=32, step=1, value=16,
296
- label="Memory chunks (ANEMOI_INFERENCE_NUM_CHUNKS)",
297
- info="Higher = less GPU memory, slightly slower. 16 works well on A100.",
298
- )
299
- run_btn = gr.Button("▶ Run Forecast", variant="primary", size="lg")
300
- status_box = gr.Textbox(
301
- label="Status", lines=3, interactive=False,
302
- placeholder="Click 'Run Forecast' to begin…",
303
- )
304
-
305
- with gr.Column(scale=2, elem_classes="panel"):
306
- gr.Markdown("### 🗺️ Visualise Output")
307
- with gr.Row():
308
- field_dd = gr.Dropdown(
309
- choices=PLOTTABLE_FIELDS,
310
- value="2t",
311
- label="Field",
312
- info="Select the variable to plot.",
313
- )
314
- timestamp_dd = gr.Dropdown(
315
- choices=[],
316
- label="Forecast step",
317
- info="Available after running the forecast.",
318
- )
319
- plot_btn = gr.Button("🖼 Plot Field", variant="secondary")
320
- map_img = gr.Image(label="Global Map", type="filepath")
321
- stats_md = gr.Markdown()
322
-
323
- # Wire up
324
- run_btn.click(
325
- fn=run_forecast,
326
- inputs=[lead_time_sl, num_chunks_sl],
327
- outputs=[status_box, timestamp_dd],
328
- )
329
-
330
- plot_btn.click(
331
- fn=plot_field,
332
- inputs=[field_dd, timestamp_dd],
333
- outputs=[map_img, stats_md],
334
- )
335
-
336
- gr.Markdown(
337
- """
338
- ---
339
- **Notes**
340
- - First run downloads the ~2 GB model checkpoint from Hugging Face and caches it.
341
- - Data is downloaded from the ECMWF Open Data API (CC BY 4.0 — please attribute ECMWF).
342
- - Results may differ slightly from operational AIFS due to GPU non-determinism and regrid differences.
343
- """
344
- )
345
-
346
 
347
- if __name__ == "__main__":
348
- demo.launch(share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import torch
3
+ import sys
4
+ import platform
5
+
6
+ def get_info():
7
+ return f"""
8
+ Python: {sys.version}
9
+ Platform: {platform.platform()}
10
+ Torch: {torch.__version__}
11
+ CUDA: {torch.version.cuda}
12
+ CXX11 ABI: {torch._C._GLIBCXX_USE_CXX11_ABI}
13
+ GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
 
16
+ with gr.Blocks() as demo:
17
+ gr.Textbox(value=get_info, every=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ demo.launch()
 
app2.py DELETED
@@ -1,19 +0,0 @@
1
- import gradio as gr
2
- import torch
3
- import sys
4
- import platform
5
-
6
- def get_info():
7
- return f"""
8
- Python: {sys.version}
9
- Platform: {platform.platform()}
10
- Torch: {torch.__version__}
11
- CUDA: {torch.version.cuda}
12
- CXX11 ABI: {torch._C._GLIBCXX_USE_CXX11_ABI}
13
- GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}
14
- """
15
-
16
- with gr.Blocks() as demo:
17
- gr.Textbox(value=get_info, every=1)
18
-
19
- demo.launch()