A1 – Preparing Reference Data for Verification#
This annex shows how to download the two reference datasets used to benchmark AIFS forecasts in the user-guide notebooks:
MeteoSwiss automatic-station observations — hourly 2 m temperature and 10 m wind speed, downloaded via meteora and stored in the stationbench format.
ERA5 reanalysis — 2 m temperature and 10 m wind speed from the Copernicus CDS API, also stored in stationbench format.
Both outputs are saved as NetCDF files and loaded lazily with xarray in the main notebooks.
Prerequisites: a valid
~/.cdsapircfile for ERA5 downloads (see the CDS API documentation).
[ ]:
import datetime as dt
import pathlib
import tempfile
import cdsapi
import numpy as np
import xarray as xr
from meteora import clients, units, utils
from shapely import geometry
Configuration#
Adjust the parameters below to match your experiment.
[ ]:
# forecast window
start_date = dt.datetime(2025, 6, 20, 0, tzinfo=dt.UTC)
lead_time = 240 # hours
end_date = start_date + dt.timedelta(hours=lead_time)
# spatial bounding box (lat/lon)
lat_slice = (44.5, 48.5)
lon_slice = (4.5, 11.5)
# output directory
data_dir = pathlib.Path("../data/stationbench")
data_dir.mkdir(parents=True, exist_ok=True)
# output file paths
experiment = "heatwave-2025-jun-ens"
stations_filepath = data_dir / f"{experiment}-stations.nc"
era5_filepath = data_dir / f"{experiment}-era5.nc"
1. MeteoSwiss station observations#
We use meteora to download hourly 2 m temperature and 10 m wind speed from MeteoSwiss automatic stations within the bounding box defined above. The observations are then converted from SI units (K, m/s) and written to a stationbench-ready NetCDF file with dimensions (time, station_id).
[ ]:
if not stations_filepath.exists():
client = clients.MeteoSwissClient(
region=geometry.box(*lon_slice, *lat_slice), crs="epsg:4326"
)
ts_df = client.get_ts_df(
variables=["temperature", "wind_speed"],
start=start_date.replace(tzinfo=None),
end=end_date.replace(tzinfo=None),
)
ts_df = units.convert_units(ts_df, {"temperature": "K"})
ts_ds = utils.long_to_stationbench(ts_df, client.stations_gdf)
ts_ds.to_netcdf(stations_filepath)
print(f"Saved {stations_filepath}")
else:
print(f"{stations_filepath} already exists; skipping")
ts_ds = xr.open_dataset(stations_filepath)
ts_ds
Saved ../data/stationbench/heatwave-2025-jun-ens-stations.nc
<xarray.Dataset> Size: 4MB
Dimensions: (time: 1441, station_id: 157)
Coordinates:
* time (time) datetime64[ns] 12kB 2025-06-20 ... 2025-06-30
* station_id (station_id) <U3 2kB 'ABO' 'AEG' 'AIG' ... 'WFJ' 'WYN' 'ZER'
longitude (station_id) float64 1kB ...
latitude (station_id) float64 1kB ...
Data variables:
2m_temperature (time, station_id) float64 2MB ...
10m_wind_speed (time, station_id) float64 2MB ...2. ERA5 reanalysis#
We download 2 m temperature and 10 m wind components from the ERA5 single-level reanalysis via the Copernicus CDS API for every 6-hourly valid time within the forecast window. The 10 m wind speed scalar is computed from the U and V components, and the dataset is stored in the stationbench format with dimensions (time, prediction_timedelta, latitude, longitude).
[ ]:
if not era5_filepath.exists():
start_date_naive = start_date.replace(tzinfo=None)
init_time = np.datetime64(start_date_naive, "ns")
valid_dts = [
start_date_naive + dt.timedelta(hours=h) for h in range(6, lead_time + 1, 6)
]
valid_times = np.array([np.datetime64(vdt, "ns") for vdt in valid_dts])
lead_times = valid_times - init_time
years = sorted({vdt.strftime("%Y") for vdt in valid_dts})
months = sorted({vdt.strftime("%m") for vdt in valid_dts})
days = sorted({vdt.strftime("%d") for vdt in valid_dts})
hours = sorted({vdt.strftime("%H:00") for vdt in valid_dts})
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_filepath = pathlib.Path(tmp_dir) / "era5_raw.nc"
cdsapi.Client().retrieve(
"reanalysis-era5-single-levels",
{
"product_type": ["reanalysis"],
"variable": [
"2m_temperature",
"10m_u_component_of_wind",
"10m_v_component_of_wind",
],
"year": years,
"month": months,
"day": days,
"time": hours,
"data_format": "netcdf",
"download_format": "unarchived",
},
str(tmp_filepath),
)
ds = xr.open_dataset(tmp_filepath)
time_dim = "valid_time" if "valid_time" in ds.dims else "time"
ds = ds.sel({time_dim: valid_times})
# rename to short names if needed
rename_map = {}
if "2m_temperature" in ds:
rename_map["2m_temperature"] = "t2m"
if "10m_u_component_of_wind" in ds:
rename_map["10m_u_component_of_wind"] = "u10"
if "10m_v_component_of_wind" in ds:
rename_map["10m_v_component_of_wind"] = "v10"
if rename_map:
ds = ds.rename(rename_map)
era5_ds = xr.Dataset(
{
"2t": (
["time", "prediction_timedelta", "latitude", "longitude"],
ds["t2m"].values[np.newaxis],
),
"10si": (
["time", "prediction_timedelta", "latitude", "longitude"],
np.sqrt(ds["u10"].values ** 2 + ds["v10"].values ** 2)[np.newaxis],
),
},
coords={
"time": [init_time],
"prediction_timedelta": lead_times,
"latitude": ds["latitude"].values,
"longitude": ds["longitude"].values,
},
)
era5_ds.to_netcdf(era5_filepath)
print(f"Saved {era5_filepath}")
else:
print(f"{era5_filepath} already exists; skipping")
era5_ds = xr.open_dataset(era5_filepath)
era5_ds
2026-03-18 11:58:39,011 INFO [2025-12-11T00:00:00] Please note that a dedicated catalogue entry for this dataset, post-processed and stored in Analysis Ready Cloud Optimized (ARCO) format (Zarr), is available for optimised time-series retrievals (i.e. for retrieving data from selected variables for a single point over an extended period of time in an efficient way). You can discover it [here](https://cds.climate.copernicus.eu/datasets/reanalysis-era5-single-levels-timeseries?tab=overview)
2026-03-18 11:58:39,015 INFO Request ID is be7d0364-571e-48b8-a97b-6851aa88506a
2026-03-18 11:58:39,065 INFO status has been updated to accepted
2026-03-18 11:58:47,382 INFO status has been updated to running
2026-03-18 11:59:11,693 INFO status has been updated to successful
Saved ../data/stationbench/heatwave-2025-jun-ens-era5.nc
<xarray.Dataset> Size: 332MB
Dimensions: (time: 1, prediction_timedelta: 40, latitude: 721,
longitude: 1440)
Coordinates:
* time (time) datetime64[ns] 8B 2025-06-20
* prediction_timedelta (prediction_timedelta) timedelta64[ns] 320B 06:00:0...
* latitude (latitude) float64 6kB 90.0 89.75 ... -89.75 -90.0
* longitude (longitude) float64 12kB 0.0 0.25 0.5 ... 359.5 359.8
Data variables:
2t (time, prediction_timedelta, latitude, longitude) float32 166MB ...
10si (time, prediction_timedelta, latitude, longitude) float32 166MB ...