Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d2a75ef
Add inference + experiment configs for new ICON-REA-L interpolator
Louis-Frey Apr 21, 2026
f60a9ab
Renamed interpolator config and switch to hourly baseline
jonasbhend Apr 22, 2026
5f86d81
Remove intermediate configs
jonasbhend Apr 22, 2026
5a24380
Add accumulation post-processor to ensure that TOT_PREC is accumulate…
jonasbhend Apr 22, 2026
ba0029b
Update checkpoints and baselines
jonasbhend Apr 22, 2026
31fcb65
Fix trailing whitespace
jonasbhend Apr 22, 2026
91cdf61
Inlined debugging comments and remove debugging text file
jonasbhend Apr 22, 2026
0088c37
move computation to inner loop to avoid dask graph bloat
jonasbhend Apr 23, 2026
9b07bd0
Use allocated CPU resources in dataset computation
jonasbhend Apr 23, 2026
7b0b247
Update config/interpolators-ich1-balfrin.yaml
jonasbhend Apr 23, 2026
7fcbf8a
Update config/interpolators-ich1-balfrin.yaml
jonasbhend Apr 23, 2026
2d25a53
renamed interpolator config
jonasbhend Apr 23, 2026
24848f1
generalize the plot of meteograms to any location
cosunae Apr 28, 2026
d162b37
Merge branch 'main' into general_meteogram
cosunae May 1, 2026
f3e1c30
config for regions and params
cosunae May 1, 2026
71a65c1
Merge branch 'main' into general_meteogram
cosunae May 4, 2026
51bb6fc
update the schema
cosunae May 4, 2026
e2db0c9
fix use a default when no params or regions are defined
cosunae May 4, 2026
1144291
linting
cosunae May 4, 2026
f2180a1
add a showcase config example to a config
cosunae May 4, 2026
c26dd24
regions -> domains
cosunae May 5, 2026
2fe543a
fix for TOT_PREC (missing on step 0)
cosunae May 5, 2026
68487ba
update the extent of alpine arc
cosunae May 5, 2026
8e76a1b
add forgotten rename
cosunae May 5, 2026
c9bb0ed
add an animation speed
cosunae May 6, 2026
c72310f
make comparisons side by side
cosunae May 7, 2026
f91cd65
organize the configs
cosunae May 7, 2026
d94e913
Merge branch 'main' into general_meteogram
cosunae May 8, 2026
8729776
fix linting
cosunae May 18, 2026
3ca16c3
Merge branch 'general_meteogram' into animation_speed
cosunae May 18, 2026
bc27168
revert config
cosunae May 18, 2026
c283580
add an include to better organize the yamls
cosunae May 18, 2026
a291c59
add tests
cosunae May 20, 2026
1ce6b77
Merge branch 'main' into animation_speed
cosunae May 27, 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 config/interpolators-ich1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ runs:
checkpoint: https://service.meteoswiss.ch/mlstore#/experiments/602/runs/c30490b6ba064e4db03b430f3a2595ad
config: resources/inference/configs/sgm-multidataset-forecaster-global-ich1-oper.yaml
steps: 0/120/6
label: stage_E
extra_requirements:
- git+https://github.com/ecmwf/anemoi-inference.git@e369b1a90313e9701db13f63364a467aa281cf36
extra_requirements:
Expand Down
27 changes: 27 additions & 0 deletions config/showcase-interpolators-ich1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Showcase config for interpolators-ich1.
# Includes the base config and overrides only the showcase section.
#
# .venv/bin/evalml showcase config/showcase-interpolators-ich1.yaml

include: interpolators-ich1.yaml

showcase:
params:
- T_2M
- SP_10M
- TOT_PREC
meteograms:
enabled: false
stations: [JUN]
animations:
enabled: true
domains:
- europe
- switzerland
- globe
speed: 10 # simulated hours per second
runs:
- Varda-Single
comparisons:
- left: Varda-Single
right: KENDA-CH1
5 changes: 4 additions & 1 deletion src/data_input/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ def load_analysis_data_from_zarr(
"PMSL": "msl",
"TOT_PREC": "tp",
}
tot_prec_string = "TOT_PREC_6H" if min(np.diff(steps)) == 6 else "TOT_PREC_1H"
_diffs = np.diff(steps)
tot_prec_string = (
"TOT_PREC_6H" if len(_diffs) > 0 and min(_diffs) == 6 else "TOT_PREC_1H"
)
PARAMS_MAP_COSMO1 = {
v: v.replace("TOT_PREC", tot_prec_string) for v in PARAMS_MAP_COSMO2.keys()
}
Expand Down
17 changes: 16 additions & 1 deletion src/evalml/cli.py
Comment thread
frazane marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,24 @@ def generate_graph(
click.echo(f"Graph saved to {output_file}")


def _deep_merge(base: dict, override: dict) -> dict:
"""Recursively merge override into base. Override wins on conflicts."""
result = dict(base)
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _deep_merge(result[key], value)
else:
result[key] = value
return result


def load_yaml(path: Path) -> dict[str, Any]:
with path.open("r") as f:
return yaml.safe_load(f)
data = yaml.safe_load(f)
if include := data.pop("include", None):
base = load_yaml(path.parent / include)
data = _deep_merge(base, data)
return data


def workflow_options(func):
Expand Down
36 changes: 34 additions & 2 deletions src/evalml/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Dict, List, Any, ClassVar, FrozenSet
from typing import Dict, List, Any, ClassVar, FrozenSet, Optional

from pydantic import BaseModel, Field, RootModel, field_validator

Expand Down Expand Up @@ -212,6 +212,12 @@ class BaselineItem(BaseModel):
baseline: BaselineConfig


class RegionConfig(BaseModel):
"""A custom map region defined by name, extent, and projection."""

name: str = Field(..., description="Name for the custom region (used as wildcard).")


class DomainConfig(BaseModel):
"""A custom map domain defined by name, extent, and projection."""

Expand All @@ -228,6 +234,13 @@ class DomainConfig(BaseModel):
model_config = {"extra": "forbid"}


class AnimationComparison(BaseModel):
"""A side-by-side comparison animation between two runs."""

left: str = Field(..., description="Label of the run shown in the left panel.")
right: str = Field(..., description="Label of the run shown in the right panel.")


class MeteogramConfig(BaseModel):
"""Configuration for meteogram generation."""

Expand All @@ -248,7 +261,7 @@ class AnimationsConfig(BaseModel):
default=True,
description="Whether to generate forecast animations (GIFs per param and region).",
)
domains: List[str | DomainConfig] = Field(
domains: List[str | RegionConfig] = Field(
default=["globe", "europe", "switzerland"],
description=(
"Domains to generate animations for. Each entry is either a named domain "
Expand All @@ -257,6 +270,25 @@ class AnimationsConfig(BaseModel):
"[lon_min, lon_max, lat_min, lat_max], and optional 'projection'."
),
)
speed: float = Field(
default=10.0,
gt=0,
description="Animation playback speed in simulated hours per second.",
)
runs: Optional[List[str]] = Field(
default=None,
description=(
"Labels of runs to generate individual animations for. "
"Defaults to all candidate runs when omitted."
),
)
comparisons: List[AnimationComparison] = Field(
default=[],
description=(
"Side-by-side two-panel comparison animations. Each entry specifies "
"the labels of the left and right panel runs."
),
)


class ShowcaseConfig(BaseModel):
Expand Down
56 changes: 55 additions & 1 deletion src/plotting/compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta
from pathlib import Path

import earthkit.data as ekd
Expand Down Expand Up @@ -77,6 +77,60 @@ def load_state_from_grib(
return state


def load_state_from_zarr(

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.

Would it make sense to wait for #153 and read from GRIB files directly?

@cosunae cosunae May 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I thought so, but unfortunately the truth seems to stay in zarr, so I am assuming we need it still to be able to plot the truth?

https://github.com/MeteoSwiss/evalml/pull/153/changes#diff-c0f704baca2ade5ded6448745abbb4de19d01d0f2a7213b0c119fef7679c6cb7R44

zarr_root: Path,
reftime: datetime,
lead_time_hours: int,
params: list[str],
source_type: str = "analysis",
) -> dict:
"""Load a single time step from a zarr source into the state dict used by StatePlotter.

Parameters
----------
zarr_root:
Path to the zarr dataset.
reftime:
Forecast reference time (init time).
lead_time_hours:
Lead time in hours to load.
params:
List of parameter names (ICON convention, e.g. ``['U_10M', 'V_10M']``).
source_type:
``'analysis'`` for truth zarrs (loads via ``load_analysis_data_from_zarr``),
``'baseline'`` for baseline forecast zarrs.
"""
from data_input import load_analysis_data_from_zarr, load_baseline_from_zarr

steps = [lead_time_hours]

if source_type == "analysis":
ds = load_analysis_data_from_zarr(zarr_root, reftime, steps, params)
ds_t = ds.isel(time=0) if "time" in ds.dims else ds.squeeze()
else:
ds = load_baseline_from_zarr(zarr_root, reftime, steps, params)
ds_t = ds.isel(lead_time=0) if "lead_time" in ds.dims else ds.squeeze()

lat = ds_t.lat.values.flatten()
lon = ds_t.lon.values.flatten()

hull = MultiPoint(list(zip(lon.tolist(), lat.tolist()))).convex_hull
state = {
"forecast_reference_time": reftime,
"valid_time": reftime + timedelta(hours=lead_time_hours),
"longitudes": lon,
"latitudes": lat,
"lam_envelope": gpd.GeoSeries([hull], crs="EPSG:4326"),
"fields": {},
}
for param in params:
if param in ds_t.data_vars:
state["fields"][param] = ds_t[param].values.flatten()
else:
state["fields"][param] = np.full(lat.size, np.nan, dtype=float)
return state


def load_state_from_raw(
file: Path, paramlist: list[str] | None = None
) -> dict[str, np.ndarray | dict[str, np.ndarray]]:
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
import pytest

from os.path import Path
from evalml.cli import _deep_merge, load_yaml
from evalml.config import ConfigModel


def test_deep_merge_override_wins():
base = {"a": 1, "b": {"x": 1, "y": 2}}
override = {"b": {"y": 99}, "c": 3}
result = _deep_merge(base, override)
assert result == {"a": 1, "b": {"x": 1, "y": 99}, "c": 3}


def test_deep_merge_non_dict_override_replaces():
base = {"a": {"x": 1}}
override = {"a": [1, 2, 3]}
result = _deep_merge(base, override)
assert result["a"] == [1, 2, 3]


def test_load_yaml_without_include(tmp_path):
f = tmp_path / "config.yaml"
f.write_text("a: 1\n")
assert load_yaml(f) == {"a": 1}


def test_load_yaml_include_merges_base(tmp_path):
base = tmp_path / "base.yaml"
base.write_text("a: 1\nb:\n x: 1\n y: 2\n")

child = tmp_path / "child.yaml"
child.write_text("include: base.yaml\nb:\n y: 99\nc: 3\n")

result = load_yaml(child)
assert result == {"a": 1, "b": {"x": 1, "y": 99}, "c": 3}


def test_load_yaml_include_validates_as_config_model():
path = Path("config/showcase-interpolators-ich1.yaml")
data = load_yaml(path)
_ = ConfigModel.model_validate(data)


def test_example_forecasters_config(example_forecasters_config):
"""Test that the example config loads correctly."""

Expand Down
14 changes: 13 additions & 1 deletion workflow/Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,26 @@ rule showcase_all:
expand(
rules.make_forecast_animation.output,
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
run_id=CANDIDATES,
run_id=SHOWCASE_ANIMATION_RUN_IDS,
param=SHOWCASE_PARAMS,
region=list(SHOWCASE_REGIONS.keys()),
showcase=EXPERIMENT_NAME,
)
if config["showcase"]["animations"]["enabled"]
else []
),
(
expand(
rules.make_comparison_animation.output,
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
comparison_id=[c["id"] for c in SHOWCASE_COMPARISONS],
param=SHOWCASE_PARAMS,
region=list(SHOWCASE_REGIONS.keys()),
showcase=EXPERIMENT_NAME,
)
if config["showcase"]["animations"]["enabled"] and SHOWCASE_COMPARISONS
else []
),
(
expand(
rules.plot_meteogram.output,
Expand Down
Loading
Loading