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 ...