diff --git a/config/interpolators-ich1.yaml b/config/interpolators-ich1.yaml index 92648b1f..0dc5e236 100644 --- a/config/interpolators-ich1.yaml +++ b/config/interpolators-ich1.yaml @@ -53,6 +53,22 @@ stratification: - alpensuedseite root: /scratch/mch/bhendj/regions/Prognoseregionen_LV95_20220517 +showcase: + params: + - T_2M + - SP_10M + - TOT_PREC + meteograms: + enabled: false + stations: [JUN] #, COV, GOR, WFJ, SAE, SAM, DAV, ZER, ANT, VSBAS, BRT, LTB, GOS, CEV, BIA] + animations: + enabled: true + domains: + - europe + - switzerland + - name: alpine_arc + extent: [3.0, 17.0, 43.5, 48.5] + projection: orthographic thresholds: TOT_PREC: gt: [0.0, 0.001, 0.005] diff --git a/src/evalml/config.py b/src/evalml/config.py index 22f50b75..1b94e53f 100644 --- a/src/evalml/config.py +++ b/src/evalml/config.py @@ -212,6 +212,70 @@ 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 MeteogramConfig(BaseModel): + """Configuration for meteogram generation.""" + + enabled: bool = Field( + default=True, + description="Whether to generate meteograms (time series plots at stations).", + ) + stations: List[str] = Field( + default=["GVE", "KLO", "LUG"], + description="List of PeakWeather station IDs to generate meteograms for.", + ) + + +class AnimationsConfig(BaseModel): + """Configuration for animation generation.""" + + enabled: bool = Field( + default=True, + description="Whether to generate forecast animations (GIFs per param and region).", + ) + 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 ShowcaseConfig(BaseModel): + """Configuration for the showcase workflow.""" + + params: List[str] = Field( + default=["T_2M", "SP_10M"], + description="List of parameters to generate animations and meteograms for.", + ) + meteograms: MeteogramConfig = Field( + default_factory=MeteogramConfig, + description="Configuration for meteogram generation.", + ) + animations: AnimationsConfig = Field( + default_factory=AnimationsConfig, + description="Configuration for animation generation.", + ) + + class Locations(BaseModel): """Locations of data and services used in the workflow.""" @@ -355,6 +419,10 @@ def validate_threshold_operators( dashboard: Dashboard locations: Locations profile: Profile + showcase: ShowcaseConfig = Field( + default_factory=ShowcaseConfig, + description="Settings for the showcase workflow.", + ) model_config = { "extra": "forbid", # fail on misspelled keys diff --git a/src/plotting/__init__.py b/src/plotting/__init__.py index ce5e4e63..810468ad 100644 --- a/src/plotting/__init__.py +++ b/src/plotting/__init__.py @@ -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 diff --git a/src/plotting/compat.py b/src/plotting/compat.py index 665287e0..7c4d14d3 100644 --- a/src/plotting/compat.py +++ b/src/plotting/compat.py @@ -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 diff --git a/workflow/Snakefile b/workflow/Snakefile index becffcb6..f08bc6f4 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -140,21 +140,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"]["enabled"] + 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"]["meteograms"]["stations"], + showcase=EXPERIMENT_NAME, + ) + if config["showcase"]["meteograms"]["enabled"] + else [] ), diff --git a/workflow/rules/common.smk b/workflow/rules/common.smk index 1bcbe580..124cceeb 100644 --- a/workflow/rules/common.smk +++ b/workflow/rules/common.smk @@ -84,6 +84,30 @@ 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("animations", {}) + .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 # ============================================================================ @@ -298,6 +322,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() diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 7eb90a59..6d35c248 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -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: ( @@ -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, @@ -100,6 +101,14 @@ 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: """ @@ -107,6 +116,7 @@ rule plot_forecast_frame: 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} \ --accu {params.accu} \ # interactive editing (needs to set localrule: True and use only one core) # marimo edit {input.script} -- \ @@ -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 diff --git a/workflow/scripts/plot_forecast_frame.mo.py b/workflow/scripts/plot_forecast_frame.mo.py index ae3021a9..e370136d 100644 --- a/workflow/scripts/plot_forecast_frame.mo.py +++ b/workflow/scripts/plot_forecast_frame.mo.py @@ -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 @@ -29,6 +30,7 @@ def _(): logging, np, DOMAINS, + get_projection, ccrs, ) @@ -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" ) @@ -208,6 +224,7 @@ def _( accu, args, get_style, + get_projection, outfn, param, preprocess_field, @@ -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), ) diff --git a/workflow/tools/config.schema.json b/workflow/tools/config.schema.json index 067dfdb2..8e0affff 100644 --- a/workflow/tools/config.schema.json +++ b/workflow/tools/config.schema.json @@ -1,5 +1,38 @@ { "$defs": { + "AnimationsConfig": { + "description": "Configuration for animation generation.", + "properties": { + "enabled": { + "default": true, + "description": "Whether to generate forecast animations (GIFs per param and region).", + "title": "Enabled", + "type": "boolean" + }, + "domains": { + "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'.", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/RegionConfig" + } + ] + }, + "title": "Domains", + "type": "array" + } + }, + "title": "AnimationsConfig", + "type": "object" + }, "BaselineConfig": { "description": "Configuration for a single baseline to include in the verification.", "properties": { @@ -453,6 +486,32 @@ "title": "Locations", "type": "object" }, + "MeteogramConfig": { + "description": "Configuration for meteogram generation.", + "properties": { + "enabled": { + "default": true, + "description": "Whether to generate meteograms (time series plots at stations).", + "title": "Enabled", + "type": "boolean" + }, + "stations": { + "default": [ + "GVE", + "KLO", + "LUG" + ], + "description": "List of PeakWeather station IDs to generate meteograms for.", + "items": { + "type": "string" + }, + "title": "Stations", + "type": "array" + } + }, + "title": "MeteogramConfig", + "type": "object" + }, "Profile": { "description": "Workflow execution profile.", "properties": { @@ -491,6 +550,69 @@ "title": "Profile", "type": "object" }, + "RegionConfig": { + "additionalProperties": false, + "description": "A custom map region defined by name, extent, and projection.", + "properties": { + "name": { + "description": "Name for the custom region (used as wildcard).", + "title": "Name", + "type": "string" + }, + "extent": { + "anyOf": [ + { + "items": { + "type": "number" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Geographic extent as [lon_min, lon_max, lat_min, lat_max] in PlateCarree coordinates. None means full globe.", + "title": "Extent" + }, + "projection": { + "default": "orthographic", + "description": "Projection name (must be a key in plotting._PROJECTIONS, e.g. 'orthographic').", + "title": "Projection", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "RegionConfig", + "type": "object" + }, + "ShowcaseConfig": { + "description": "Configuration for the showcase workflow.", + "properties": { + "params": { + "default": [ + "T_2M", + "SP_10M" + ], + "description": "List of parameters to generate animations and meteograms for.", + "items": { + "type": "string" + }, + "title": "Params", + "type": "array" + }, + "meteograms": { + "$ref": "#/$defs/MeteogramConfig" + }, + "animations": { + "$ref": "#/$defs/AnimationsConfig" + } + }, + "title": "ShowcaseConfig", + "type": "object" + }, "Stratification": { "description": "Stratification settings for the analysis.", "properties": { @@ -632,6 +754,10 @@ }, "profile": { "$ref": "#/$defs/Profile" + }, + "showcase": { + "$ref": "#/$defs/ShowcaseConfig", + "description": "Settings for the showcase workflow." } }, "required": [