Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
29 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
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
b9a5242
Merge branch 'main' into general_meteogram
cosunae May 19, 2026
25a066b
Merge branch 'main' into general_meteogram
cosunae May 22, 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
16 changes: 16 additions & 0 deletions config/interpolators-ich1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ stratification:
- alpensuedseite
root: /scratch/mch/bhendj/regions/Prognoseregionen_LV95_20220517

showcase:
params:
- T_2M
- SP_10M
- TOT_PREC
meteograms: false
animations: true
domains:
- europe
- switzerland
- name: alpine_arc
extent: [3.0, 17.0, 43.5, 48.5]
projection: orthographic

stations: [JUN] #, COV, GOR, WFJ, SAE, SAM, DAV, ZER, ANT, VSBAS, BRT, LTB, GOS, CEV, BIA]
Comment thread
jonasbhend marked this conversation as resolved.
Outdated

locations:
output_root: output/

Expand Down
7 changes: 5 additions & 2 deletions src/data_input/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,14 @@ def load_fct_data_from_grib(
ds[var] = da.rename({"z": da.attrs["vcoord_type"]})
ds = xr.merge([ds[p].rename(p) for p in ds], compat="no_conflicts")
lead_times = np.array(steps, dtype="timedelta64[h]")
# Restrict to the requested lead times so that the TOT_PREC disaggregation
# Reindex to the requested lead times so that the TOT_PREC disaggregation
# below operates on the correct step interval even if the GRIB contains
# extra (e.g. hourly) steps beyond those requested — e.g. when consuming
# output from an interpolator emulator or a baseline with sub-step output.
ds = ds.sel(lead_time=lead_times)
# reindex (rather than sel) fills missing steps with NaN instead of raising
# KeyError — needed because accumulated fields like TOT_PREC are absent at
# step 0 in the GRIB, and the NaN-fill logic below handles that case.
ds = ds.reindex(lead_time=lead_times)
if "TOT_PREC" in ds.data_vars:
## Disaggregate TOT_PREC from cumulative-from-start (expected when the
## accumulate_from_start_of_forecast post-processor is enabled in
Expand Down
50 changes: 50 additions & 0 deletions src/evalml/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,52 @@ 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).")
extent: List[float] | None = Field(
None,
description="Geographic extent as [lon_min, lon_max, lat_min, lat_max] in PlateCarree coordinates. None means full globe.",
)
projection: str = Field(
"orthographic",
description="Projection name (must be a key in plotting._PROJECTIONS, e.g. 'orthographic').",
)

model_config = {"extra": "forbid"}


class ShowcaseConfig(BaseModel):
"""Configuration for the showcase workflow."""

meteograms: bool = Field(
default=True,
description="Whether to generate meteograms (time series plots at stations).",
)
animations: bool = Field(
default=True,
description="Whether to generate forecast animations (GIFs per param and region).",
)
params: List[str] = Field(
default=["T_2M", "SP_10M"],
description="List of parameters to generate animations and meteograms for.",
)
stations: List[str] = Field(
default=["GVE", "KLO", "LUG"],
description="List of PeakWeather station IDs to generate meteograms for.",
)
domains: List[str | RegionConfig] = Field(
default=["globe", "europe", "switzerland"],
description=(
"Domains to generate animations for. Each entry is either a named domain "
"(e.g. 'globe', 'europe', 'switzerland') defined in plotting.DOMAINS, "
"or a custom domain dict with 'name', optional 'extent' "
"[lon_min, lon_max, lat_min, lat_max], and optional 'projection'."
),
)


class Locations(BaseModel):
"""Locations of data and services used in the workflow."""

Expand Down Expand Up @@ -318,6 +364,10 @@ class ConfigModel(BaseModel):
stratification: Stratification
locations: Locations
profile: Profile
showcase: ShowcaseConfig = Field(
default_factory=ShowcaseConfig,
description="Settings for the showcase workflow.",
)

model_config = {
"extra": "forbid", # fail on misspelled keys
Expand Down
9 changes: 9 additions & 0 deletions src/plotting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@

# Mapping of region names to their geographic extent and projection
# extent [lon_min, lon_max, lat_min, lat_max] in PlateCarree coordinates
def get_projection(name: str) -> "ccrs.Projection":
"""Look up a projection by name."""
if name not in _PROJECTIONS:
raise ValueError(
f"Unknown projection {name!r}. Available: {list(_PROJECTIONS)}"
)
return _PROJECTIONS[name]


DOMAINS = {
"globe": {
"extent": None, # full globe view
Expand Down
11 changes: 9 additions & 2 deletions src/plotting/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ def load_state_from_grib(
fds = data_source.FileDataSource(datafiles=[str(file)])
ds = grib_decoder.load(fds, {"param": paramlist})
state = {}
lats = ds[paramlist[0]].lat.data.flatten()
lons = ds[paramlist[0]].lon.data.flatten()
ref_param = next((p for p in (paramlist or []) if p in ds), None)
if ref_param is None:
raise ValueError(
f"None of the requested params {paramlist} found in {file}. "
"The GRIB file may not contain these fields at this lead time "
"(e.g. accumulated fields like TOT_PREC are undefined at step 0)."
)
lats = ds[ref_param].lat.data.flatten()
lons = ds[ref_param].lon.data.flatten()
state["forecast_reference_time"] = reftime
state["valid_time"] = reftime + pd.to_timedelta(lead_time_hours, unit="h")
state["longitudes"] = lons
Expand Down
36 changes: 22 additions & 14 deletions workflow/Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,29 @@ rule experiment_all:
rule showcase_all:
"""Target rule for showcase workflow."""
input:
expand(
rules.make_forecast_animation.output,
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
run_id=CANDIDATES,
param=["T_2M", "SP_10M", "TOT_PREC"],
region=["globe", "europe", "switzerland"],
showcase=EXPERIMENT_NAME,
(
expand(
rules.make_forecast_animation.output,
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
run_id=CANDIDATES,
param=SHOWCASE_PARAMS,
region=list(SHOWCASE_REGIONS.keys()),
showcase=EXPERIMENT_NAME,
)
if config["showcase"]["animations"]
else []
),
expand(
rules.plot_meteogram.output,
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
run_id=CANDIDATES,
param=["T_2M", "SP_10M"],
sta=["GVE", "KLO", "LUG"],
showcase=EXPERIMENT_NAME,
(
expand(
rules.plot_meteogram.output,
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
run_id=CANDIDATES,
param=SHOWCASE_PARAMS,
sta=config["showcase"]["stations"],
showcase=EXPERIMENT_NAME,
)
if config["showcase"]["meteograms"]
else []
),


Expand Down
24 changes: 24 additions & 0 deletions workflow/rules/common.smk
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,28 @@ def parse_regions():
return regions_txt


def parse_showcase_regions():
"""Parse showcase domains from config.

Returns a dict mapping domain name -> {extent, projection}.
Named domains (strings) have extent=None and projection=None,
meaning the plot script will fall back to the DOMAINS lookup.
Custom domains carry their explicit extent and projection.
"""
result = {}
for r in config.get("showcase", {}).get(
"domains", ["globe", "europe", "switzerland"]
):
if isinstance(r, str):
result[r] = {"extent": None, "projection": None}
else:
result[r["name"]] = {
"extent": r.get("extent"),
"projection": r.get("projection", "orthographic"),
}
return result


# ============================================================================
# Run entries configuration management
# ============================================================================
Expand Down Expand Up @@ -294,6 +316,8 @@ def master_hash() -> str:


REGIONS = parse_regions()
SHOWCASE_REGIONS = parse_showcase_regions()
SHOWCASE_PARAMS = config.get("showcase", {}).get("params", ["T_2M", "SP_10M"])
REFTIMES = parse_reference_times()
RUN_CONFIGS = collect_all_runs()
ENV_CONFIGS = collect_all_envs()
Expand Down
24 changes: 20 additions & 4 deletions workflow/rules/plot.smk
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ rule plot_meteogram:
resources:
slurm_partition="postproc",
cpus_per_task=1,
runtime="10m",
runtime="60m",
params:
ana_label=lambda wc: config["truth"]["label"],
fcst_grib=lambda wc: (
Expand Down Expand Up @@ -92,6 +92,7 @@ rule plot_forecast_frame:
/ "data/runs/{run_id}/{init_time}/frames/frame_{leadtime}_{param}_{region}.png",
wildcard_constraints:
leadtime=r"\d+", # only digits
region="|".join(map(re.escape, SHOWCASE_REGIONS.keys())),
resources:
slurm_partition="postproc",
cpus_per_task=1,
Expand All @@ -100,13 +101,22 @@ rule plot_forecast_frame:
grib_out_dir=lambda wc: (
Path(OUT_ROOT) / f"data/runs/{wc.run_id}/{wc.init_time}/grib"
).resolve(),
region_extra=lambda wc: (
"--extent {} --projection {}".format(
" ".join(map(str, SHOWCASE_REGIONS[wc.region]["extent"])),
SHOWCASE_REGIONS[wc.region]["projection"],
)
if SHOWCASE_REGIONS.get(wc.region, {}).get("extent") is not None
else ""
),
accu=lambda wc: int(RUN_CONFIGS[wc.run_id]["steps"].split("/")[2]),
shell:
"""
export ECCODES_DEFINITION_PATH=$(realpath .venv/share/eccodes-cosmo-resources/definitions)
python {input.script} \
--input {params.grib_out_dir} --date {wildcards.init_time} --outfn {output[0]} \
--param {wildcards.param} --leadtime {wildcards.leadtime} --region {wildcards.region} \
{params.region_extra} \
Comment thread
jonasbhend marked this conversation as resolved.
--accu {params.accu} \
# interactive editing (needs to set localrule: True and use only one core)
# marimo edit {input.script} -- \
Expand All @@ -127,11 +137,17 @@ def get_leadtimes(wc):

rule make_forecast_animation:
localrule: True
wildcard_constraints:
param="|".join(map(re.escape, SHOWCASE_PARAMS)),
region="|".join(map(re.escape, SHOWCASE_REGIONS.keys())),
input:
expand(
lambda wc: expand(
rules.plot_forecast_frame.output,
leadtime=lambda wc: get_leadtimes(wc),
allow_missing=True,
run_id=wc.run_id,
init_time=wc.init_time,
param=wc.param,
region=wc.region,
leadtime=get_leadtimes(wc),
),
output:
OUT_ROOT
Expand Down
27 changes: 25 additions & 2 deletions workflow/scripts/plot_forecast_frame.mo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def _():
import numpy as np

from plotting import DOMAINS
from plotting import get_projection
from plotting import StatePlotter
from plotting.colormap_defaults import CMAP_DEFAULTS
from plotting.compat import load_state_from_grib
Expand All @@ -29,6 +30,7 @@ def _():
logging,
np,
DOMAINS,
get_projection,
ccrs,
)

Expand All @@ -53,6 +55,20 @@ def _(ArgumentParser, Path):
parser.add_argument("--leadtime", type=str, help="leadtime")
parser.add_argument("--param", type=str, help="parameter")
parser.add_argument("--region", type=str, help="name of region")
parser.add_argument(
"--extent",
type=float,
nargs=4,
default=None,
metavar=("LON_MIN", "LON_MAX", "LAT_MIN", "LAT_MAX"),
help="custom geographic extent in PlateCarree coordinates; overrides DOMAINS lookup",
)
parser.add_argument(
"--projection",
type=str,
default=None,
help="projection name (e.g. 'orthographic'); used only together with --extent",
)
parser.add_argument(
"--accu", type=int, default=1, help="accumulation period in hours"
)
Expand Down Expand Up @@ -208,6 +224,7 @@ def _(
accu,
args,
get_style,
get_projection,
outfn,
param,
preprocess_field,
Expand All @@ -222,11 +239,17 @@ def _(
state["latitudes"],
outfn.parent,
)
if args.extent is not None:
_projection = get_projection(args.projection or "orthographic")
_extent = args.extent
else:
_projection = DOMAINS[region]["projection"]
_extent = DOMAINS[region]["extent"]
fig = plotter.init_geoaxes(
nrows=1,
ncols=1,
projection=DOMAINS[region]["projection"],
bbox=DOMAINS[region]["extent"],
projection=_projection,
bbox=_extent,
name=region,
size=(6, 6),
)
Expand Down
Loading
Loading