Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
21f6eb2
Experiment workflow works
dnerini Jun 30, 2026
d8acf3c
Add example config
dnerini Jun 30, 2026
48c98bb
Allow for no baselines in meteograms
dnerini Jun 30, 2026
7032716
fix: fix global truth units for precip, to fix verification metrics
MicheleCattaneo Jul 1, 2026
ec82cfd
Use the rescaling postprocessor for handling unit conversions
dnerini Jul 2, 2026
d308e5c
feat: support for cloud cover and rotating gifs
MicheleCattaneo Jul 3, 2026
6ab5748
Merge branch 'main' into feat/support-global-eval
MicheleCattaneo Jul 3, 2026
5e749f6
Document issue with missing coordinates
dnerini Jul 3, 2026
acac66e
Add support flexible verification regions via config
dnerini Jul 3, 2026
ff74be9
fix: fix bug for contorf plot for CLCT
MicheleCattaneo Jul 3, 2026
ecc2a80
Merge branch 'feat/support-global-eval' of github.com:MeteoSwiss/eval…
MicheleCattaneo Jul 3, 2026
3b4b240
fix: regenerated config JSON schema for new animation params
MicheleCattaneo Jul 3, 2026
1ef6528
chore: added support for plotting radiation and fixed plotting bug fo…
MicheleCattaneo Jul 13, 2026
7da9067
chore: added n320 global config
MicheleCattaneo Jul 13, 2026
8227668
chore: added ssrd in experiment configs
MicheleCattaneo Jul 17, 2026
ca6c275
Merge branch 'main' into feat/support-global-eval
dnerini Jul 20, 2026
b5cb08e
Merge branch 'main' into feat/support-global-eval
dnerini Jul 21, 2026
f99b4b1
Fix region parsing
dnerini Jul 21, 2026
6d27edc
Linting
dnerini Jul 21, 2026
8498383
Merge branch 'main' into feat/support-global-eval
dnerini Jul 22, 2026
2eee400
Fix aifs-single
dnerini Jul 22, 2026
4d3752e
Aesthetics
dnerini Jul 22, 2026
05657be
Verify aifs against era5
dnerini Jul 22, 2026
69099e2
Make regions explicit
dnerini Jul 22, 2026
3e29794
Fixes
dnerini Jul 22, 2026
2a614d0
Remove Michele's configs
dnerini Jul 22, 2026
672fb7a
Remove leftovers following 2a614d0
dnerini Jul 22, 2026
41256c7
Rename domain to 'icon'
dnerini Jul 24, 2026
05ba8af
Update comment
dnerini Jul 24, 2026
4df7af2
Fix docstrings
dnerini Jul 24, 2026
e9fd7dc
Use TOT_PREC6
dnerini Jul 24, 2026
9b1f1f4
Add predefined verif regions
dnerini Jul 24, 2026
c9c0ac6
Linting
dnerini Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ truth:
experiment:
stratification:
regions:
- icon # first item will be used as default verification region
- jura
- mittelland
- voralpen
Expand Down
15 changes: 8 additions & 7 deletions config/aifs-single.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ runs:
config: resources/inference/configs/aifs-single-forecaster.yaml
extra_requirements:
- torch-geometric==2.4.0
- anemoi-inference==0.6.3
- anemoi-inference==0.11.1
- anemoi-models==0.5.0
- https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp312-cp312-linux_x86_64.whl
disable_local_eccodes_definitions: true
inference_resources:
slurm_partition: preemptible
gpus: 1

truth:
label: KENDA-CH1
root: /store_new/mch/msopr/ml/datasets/mch-ich1-1km-2024-2025-1h-pl13-v1.0.zarr
label: ERA5-o96
root: /store_new/mch/msopr/ml/datasets/aifs-ea-an-oper-0001-mars-o96-1979-2024-1h-v3-with-era51.zarr

lapse_rate_correction: false

experiment:
params:
Expand All @@ -41,7 +41,8 @@ experiment:
# - init_hour
- season
stratification:
regions: []
regions:
- global

locations:
output_root: output/
Expand Down
1 change: 1 addition & 0 deletions config/forecasters-ich1-oper-fixed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ experiment:
- TOT_PREC6
stratification:
regions:
- icon
- jura
- mittelland
- voralpen
Expand Down
1 change: 1 addition & 0 deletions config/forecasters-ich1-oper.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ experiment:
- TOT_PREC6
stratification:
regions:
- icon
- jura
- mittelland
- voralpen
Expand Down
1 change: 1 addition & 0 deletions config/forecasters-ich1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ experiment:
- PMSL
stratification:
regions:
- icon
- jura
- mittelland
- voralpen
Expand Down
1 change: 1 addition & 0 deletions config/forecasters-ich1_mec_ffv2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ experiment:
- TOT_PREC
stratification:
regions:
- icon
- jura
root: /store_new/mch/msopr/ml/regions/Prognoseregionen_LV95_20220517
thresholds:
Expand Down
1 change: 1 addition & 0 deletions config/varda-single-1.0.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ experiment:
- PMSL
stratification:
regions:
- icon
- mittelland
- berge
- alpennordseite
Expand Down
9 changes: 8 additions & 1 deletion resources/inference/configs/aifs-single-forecaster.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
input: test
input:
test:
use_original_paths: true

allow_nans: true

Expand All @@ -12,6 +14,11 @@ post_processors:
- accumulate_from_start_of_forecast:
accumulations:
- tp
- forward_transform_filter:
rescale:
scale: 1000 # convert units from m to kg m-2
offset: 0
param: tp

output:
grib:
Expand Down
40 changes: 37 additions & 3 deletions src/data_input/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@
"2d": "TD_2M",
"sp": "PS",
"lsm": "FR_LAND",
"z": "FSI",
"tcc": "CLCT",
"lcc": "CLCL",
Comment thread
jonasbhend marked this conversation as resolved.
# TODO: ssrd is treated as a plain per-step field (no de-accumulation),
# which only holds because it's not currently listed in any
# accumulate_from_start_of_forecast.accumulations in the inference
# configs (unlike tp, see _tot_prec_handling). If ssrd/strd are ever
# added there, this needs the same cumulative-since-start handling tp
# gets, or verification/plots will silently be wrong.
Comment on lines +31 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we already produce this, but in fact it is accumulated (or not)? I am confused...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for my enlightenment, not really important.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my understanding is that we predict the same variable used during training, which is period accumulated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry missed the comment.
We produce it only on some global experiments are the moment. The unit is W/m^2, and Watts are depending on a time of accumulation, so it's similar to precipitation in that way, and it could be produced with the accumulation from the start of the forecast just like precip. For simplicity now I treated is as an instantaneous variable (and the accumulation depends on the dataset used, 1h or 6h)

"ssrd": "SSRD",
"z": "FIS",
}
_ICON_TO_IFS = {v: k for k, v in _IFS_TO_ICON.items()}

Expand Down Expand Up @@ -324,6 +333,20 @@ def _open_analysis_zarr(root: Path, params: list[str]) -> xr.Dataset:
)
ds = ds.assign_coords(elevation=elevation).drop_vars(["FIS"])

# Drop grid points with undefined (NaN) coordinates. This can occur when
# xarray opens a zarr dataset whose lat/lon arrays have fill_value=0.0: any
# grid point sitting exactly on 0° longitude is masked to NaN by xarray even
# though it is a valid point in the raw zarr (e.g. aifs-ea-an-oper o96 ERA5
# dataset has 192 such points).
if "values" in ds.dims and "latitude" in ds.coords and "longitude" in ds.coords:
valid = np.isfinite(ds["latitude"].values) & np.isfinite(ds["longitude"].values)
if not valid.all():
LOG.warning(
"Dropping %d grid point(s) with undefined lat/lon from truth dataset.",
int((~valid).sum()),
)
ds = ds.isel(values=valid)

Comment thread
dnerini marked this conversation as resolved.
return ds


Expand Down Expand Up @@ -464,10 +487,21 @@ def load_from_grib_file(file: str | list[str], sel_kwargs):


def variable_name_profile(
level_type: Literal["height_above_ground_level", "mean_sea", "surface", "pressure"],
level_type: Literal[
"height_above_ground_level",
"mean_sea",
"surface",
"pressure",
"entire_atmosphere",
],
) -> dict[str, Any]:
"""Resolve variable name profile based on the level type."""
if level_type in ["height_above_ground_level", "mean_sea", "surface"]:
if level_type in [
"height_above_ground_level",
"mean_sea",
"surface",
"entire_atmosphere",
]:
return {}
elif level_type == "pressure":
return {
Expand Down
63 changes: 59 additions & 4 deletions src/evalml/config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from pathlib import Path
from typing import Dict, List, Any, ClassVar, FrozenSet, Optional
from typing import Dict, List, Any, ClassVar, FrozenSet, Optional, Union

from pydantic import BaseModel, Field, RootModel, field_validator, model_validator

PROJECT_ROOT = Path(__file__).parents[2]

PREDEFINED_REGIONS: Dict[str, List[float]] = {
"global": [-180, 180, -90, 90],
"icon": [1.5, 16, 43, 49.5],
}


class Dates(BaseModel):
"""Start/stop of the hindcast period and the launch frequency."""
Expand Down Expand Up @@ -273,9 +278,29 @@ class DomainConfig(BaseModel):
"orthographic",
description="Projection name (must be a key in plotting._PROJECTIONS, e.g. 'orthographic').",
)
rotate: bool = Field(
False,
description=(
"Rotate the viewpoint across animation frames as lead time advances. "
"Only valid for full-globe domains (extent: null)."
),
)
hours_per_revolution: float = Field(
96.0,
gt=0,
description="Simulated lead-time hours for one full 360° rotation, when rotate is enabled.",
)

model_config = {"extra": "forbid"}

@model_validator(mode="after")
def _rotate_requires_globe(self):
if self.rotate and self.extent is not None:
raise ValueError(
"rotate: true is only valid for full-globe domains (extent: null)."
)
return self


class MeteogramConfig(BaseModel):
"""Configuration for meteogram generation."""
Expand Down Expand Up @@ -382,15 +407,45 @@ class Locations(BaseModel):
class Stratification(BaseModel):
"""Stratification settings for the analysis."""

regions: List[str] = Field(
regions: List[Union[str, Dict[str, List[float]]]] = Field(
default_factory=list,
description="List of region names for stratification. Empty list means no spatial stratification.",
description=(
"List of region specs for spatial stratification. At least one region is required. "
f"String entries are either predefined region names ({list(PREDEFINED_REGIONS)}) or "
"shapefile names resolved against 'root'. Predefined names take precedence over shapefiles. "
"Dict entries map a custom region name to a bounding box [lon_min, lon_max, lat_min, lat_max]. "
"The first entry is the domain region used by the dashboard when region stratification is not active."
),
)
root: Optional[str] = Field(
None,
description="Root directory where the region shapefiles are stored. Required when regions is non-empty.",
description="Root directory where the region shapefiles are stored. Required when regions contains string entries.",
)

@field_validator("regions")
@classmethod
def validate_regions(
cls, v: List[Union[str, Dict[str, List[float]]]]
) -> List[Union[str, Dict[str, List[float]]]]:
if not v:
raise ValueError(
"At least one region must be specified. "
f"Add a domain region as the first entry, e.g. a predefined name "
f"({list(PREDEFINED_REGIONS)}), a custom bbox dict, or a shapefile name."
)
for entry in v:
if isinstance(entry, dict):
if len(entry) != 1:
raise ValueError(
f"Each bbox region dict must have exactly one key, got: {list(entry.keys())}"
)
name, bbox = next(iter(entry.items()))
if len(bbox) != 4:
raise ValueError(
f"Bbox for region '{name}' must have exactly 4 values [lon_min, lon_max, lat_min, lat_max], got {len(bbox)}."
)
return v


class Dashboard(BaseModel):
"""Settings for the dashboard"""
Expand Down
33 changes: 21 additions & 12 deletions src/plotting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from contextlib import contextmanager
from functools import cached_property
from pathlib import Path

import cartopy.crs as ccrs
Expand Down Expand Up @@ -148,6 +147,7 @@ def plot_field(
style: ekp.styles.Style | None = None,
colorbar: bool = True,
title: str | None = None,
gridline_labels: bool = True,
**kwargs,
):
"""Plot a field on a Map object.
Expand All @@ -164,6 +164,12 @@ def plot_field(
Whether to plot a colorbar, by default True.
title: str, optional
Map subplot title.
gridline_labels : bool
Whether to draw lat/lon degree labels on the gridlines, by
default True. Set to False for views whose center rotates
between frames (e.g. a rotating globe animation), where the
labels' varying width would otherwise make the map's position
shift from frame to frame.
kwargs : dict
Additional keyword arguments to pass to ax.tripcolor, including cmap,
vmin, vmax, etc.
Expand All @@ -174,8 +180,8 @@ def plot_field(
# of the plotting function is a lot faster than letting tricontourf or
# tripcolor handle it in general, but not sure if using earthkit
# removed for now to simplify the workflow
if proj == _PROJECTIONS["orthographic"]:
triang, mask = self._orthographic_tri
if isinstance(proj, ccrs.Orthographic):
triang, mask = self._orthographic_tri(proj)
else:
triang, mask = self.tri, slice(None, None)
x, y = triang.x, triang.y
Expand Down Expand Up @@ -215,7 +221,13 @@ def plot_field(
# TODO: gridlines etc would be nicer to have in the init, but I didn't get
# them to overlay the plot layer

subplot.standard_layers()
if gridline_labels:
subplot.standard_layers()
else:
subplot.land()
subplot.coastlines()
subplot.borders()
subplot.gridlines(draw_labels=False)

if colorbar:
subplot.legend()
Expand Down Expand Up @@ -265,13 +277,10 @@ def _temporary_plot_kwargs_override(self, subplot: ekp.Map):
except Exception:
pass

@cached_property
def _orthographic_tri(self) -> Triangulation:
"""Compute the triangulation for the orthographic projection."""
x, y, _ = (
_PROJECTIONS["orthographic"]
.transform_points(ccrs.PlateCarree(), self.lon, self.lat)
.T
)
def _orthographic_tri(
self, proj: ccrs.Projection
) -> tuple[Triangulation, np.ndarray]:
"""Compute the triangulation for an orthographic-family projection."""
x, y, _ = proj.transform_points(ccrs.PlateCarree(), self.lon, self.lat).T
mask = ~(np.isnan(x) | np.isnan(y))
return Triangulation(x[mask], y[mask]), mask
32 changes: 32 additions & 0 deletions src/plotting/colormap_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ def _precip_bias_map(accum_h: int) -> dict:
"extend": "both",
},
"QV_925": load_ncl_colormap("RH_6lev.ct") | {"extend": "both"},
"CLCT": {
# extend="neither" relies on preprocess_field() clipping away from
# exact 0/1 (see plot_forecast_frame.py) to avoid a tricontourf bug
# on orthographic projections.
"cmap": plt.get_cmap("Blues_r"),
"vmin": 0,
"vmax": 1,
"extend": "neither",
"units": "",
"levels": list(np.linspace(0, 1, 21)),
},
"CLCL": {
"cmap": plt.get_cmap("Blues_r"),
"vmin": 0,
"vmax": 1,
"extend": "neither",
"units": "",
"levels": list(np.linspace(0, 1, 21)),
},
"SSRD": {
# tricontourf always bands regardless of "levels" being set (it falls
# back to an auto locator with ~7 bands otherwise) — use a fine level
# set here to approximate a smooth gradient instead. extend="max"
# only (not "both") since preprocess_field() already clips away from
# exact 0 — see CLCT.
"cmap": plt.get_cmap("YlOrRd"),
"vmin": 0,
"vmax": 4e6,
"extend": "max",
"units": "J m-2",
"levels": list(np.linspace(0, 4e6, 21)),
},
"TOT_PREC_1H": {
"extend": "max",
"colors": [
Expand Down
8 changes: 8 additions & 0 deletions src/plotting/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"PS": "sp",
"PMSL": "msl",
"TOT_PREC": "tp",
"CLCT": "tcc",
"CLCL": "lcc",
"SSRD": "ssrd",
}

PARAMS_MAP_INV = {v: k for k, v in PARAMS_MAP.items()}
Expand All @@ -35,6 +38,11 @@ def load_state_from_grib(
}
if ifs_rename:
ds = ds.rename(ifs_rename)
# TODO check if needed
# if "tp" in ifs_rename and "TOT_PREC" in ds:
# # IFS/ECMWF convention: "tp" is accumulated precip in meters.
# # Convert to kg m-2 (mm) to match the ICON-native convention.
# ds["TOT_PREC"] = ds["TOT_PREC"] * 1000
state = {}
ref_param = next((p for p in (paramlist or []) if p in ds), None)
if ref_param is None:
Expand Down
Loading
Loading