Skip to content

Commit d018591

Browse files
committed
Plot meteograms
1 parent a20ae75 commit d018591

File tree

4 files changed

+234
-2
lines changed

4 files changed

+234
-2
lines changed

config/showcase.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ baselines:
3131

3232
analysis:
3333
label: COSMO KENDA
34-
analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-6h-v3-pl13.zarr
34+
analysis_zarr: /scratch/mch/fzanetta/data/anemoi/datasets/mch-co2-an-archive-0p02-2015-2020-1h-v3-pl13.zarr
3535

3636
locations:
3737
output_root: output/

workflow/Snakefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,18 @@ rule showcase_all:
5151
OUT_ROOT
5252
/ "showcases/{run_id}/{init_time}/{init_time}_{param}_{projection}_{region}.gif",
5353
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
54-
run_id=collect_all_runs(),
54+
run_id=collect_all_candidates(),
5555
param=["2t", "10sp"],
5656
projection="orthographic",
5757
region=["none", "switzerland"],
5858
),
59+
expand(
60+
OUT_ROOT / "showcases/{run_id}/meteograms/{init_time}_{param}_{sta}.png",
61+
init_time=[t.strftime("%Y%m%d%H%M") for t in REFTIMES],
62+
run_id=collect_all_candidates(),
63+
param=["2t"],
64+
sta=["GVE", "KLO", "LUG"],
65+
),
5966

6067

6168
rule sandbox_all:

workflow/rules/plot.smk

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@ rule plot_forecast_frame:
3737
"""
3838

3939

40+
rule plot_meteogram:
41+
input:
42+
script="workflow/scripts/plot_meteogram.mo.py",
43+
fct_grib=rules.inference_routing.output[0],
44+
analysis_zarr=config["analysis"].get("analysis_zarr"),
45+
output:
46+
OUT_ROOT / "showcases/{run_id}/meteograms/{init_time}_{param}_{sta}.png",
47+
# localrule: True
48+
resources:
49+
slurm_partition="postproc",
50+
cpus_per_task=1,
51+
runtime="5m",
52+
shell:
53+
"""
54+
export ECCODES_DEFINITION_PATH=/user-environment/share/eccodes-cosmo-resources/definitions
55+
python {input.script} \
56+
--forecast {input.fct_grib} --analysis {input.analysis_zarr} \
57+
--date {wildcards.init_time} --outfn {output[0]} \
58+
--param {wildcards.param} --station {wildcards.sta}
59+
# interactive editing (needs to set localrule: True and use only one core)
60+
# marimo edit {input.script} -- \
61+
# --forecast {input.fct_grib} --analysis {input.analysis_zarr} \
62+
# --date {wildcards.init_time} --outfn {output[0]} \
63+
# --param {wildcards.param} --station {wildcards.sta}
64+
"""
65+
66+
4067
def get_leadtimes(wc):
4168
"""Get all lead times from the run config."""
4269
start, end, step = map(int, RUN_CONFIGS[wc.run_id]["steps"].split("/"))
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import marimo
2+
3+
__generated_with = "0.16.5"
4+
app = marimo.App(width="medium")
5+
6+
7+
@app.cell
8+
def _():
9+
import logging
10+
from argparse import ArgumentParser
11+
from pathlib import Path
12+
13+
import earthkit.plots as ekp
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
import pandas as pd
17+
import xarray as xr
18+
19+
from meteodatalab import data_source, grib_decoder
20+
return (
21+
ArgumentParser,
22+
Path,
23+
data_source,
24+
grib_decoder,
25+
logging,
26+
np,
27+
pd,
28+
plt,
29+
xr,
30+
)
31+
32+
33+
@app.cell
34+
def _(logging):
35+
LOG = logging.getLogger(__name__)
36+
LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
37+
logging.basicConfig(level=logging.INFO, format=LOG_FMT)
38+
return
39+
40+
41+
@app.cell
42+
def _(ArgumentParser, Path):
43+
ROOT = Path(__file__).parent
44+
45+
parser = ArgumentParser()
46+
47+
parser.add_argument(
48+
"--forecast", type=str, default=None, help="Directory to forecast grib data"
49+
)
50+
parser.add_argument(
51+
"--analysis", type=str, default=None, help="Path to analysis zarr data"
52+
)
53+
parser.add_argument("--date", type=str, default=None, help="reference datetime")
54+
parser.add_argument("--outfn", type=str, help="output filename")
55+
parser.add_argument("--param", type=str, help="parameter")
56+
parser.add_argument("--station", type=str, help="station")
57+
58+
args = parser.parse_args()
59+
grib_dir = Path(args.forecast)
60+
zarr_dir = Path(args.analysis)
61+
init_time = args.date
62+
outfn = Path(args.outfn)
63+
station = args.station
64+
param = args.param
65+
return grib_dir, init_time, outfn, param, station, zarr_dir
66+
67+
68+
@app.cell
69+
def _(pd):
70+
stations = pd.DataFrame(
71+
[
72+
("BAS", "1_75", 1, 75, "BAS", "Basel / Binningen", 1, "MeteoSchweiz", 7.583, 47.541, 316.14, 12.0, 0.0),
73+
("LUG", "1_47", 1, 47, "LUG", "Lugano", 1, "MeteoSchweiz", 8.960, 46.004, 272.56, 10.0, 27.34),
74+
("GVE", "1_58", 1, 58, "GVE", "Gen\u00e8ve / Cointrin", 1, "MeteoSchweiz", 6.122, 46.248, 415.53, 10.0, 0.0),
75+
("GUT", "1_79", 1, 79, "GUT", "G\u00fcttingen", 1, "MeteoSchweiz", 9.279, 47.602, 439.78, 12.0, 0.0),
76+
("KLO", "1_59", 1, 59, "KLO", "Z\u00fcrich / Kloten", 1, "MeteoSchweiz", 8.536, 47.48, 435.92, 10.5, 0.0),
77+
("SCU", "1_30", 1, 30, "SCU", "Scuol", 1, "MeteoSchweiz", 10.283, 46.793, 1304.42, 10.0, 0.0),
78+
("LUZ", "1_68", 1, 68, "LUZ", "Luzern", 1, "MeteoSchweiz", 8.301, 47.036, 454.0, 8.41, 32.51),
79+
("DIS", "1_54", 1, 54, "DIS", "Disentis", 1, "MeteoSchweiz", 8.853, 46.707, 1198.03, 10.0, 0.0),
80+
("PMA", "1_862", 1, 862, "PMA", "Piz Martegnas", 1, "MeteoSchweiz", 9.529, 46.577, 2668.34, 10.0, 0.0),
81+
("CEV", "1_843", 1, 843, "CEV", "Cevio", 1, "MeteoSchweiz", 8.603, 46.32, 420.0, 10.0, 6.85),
82+
("MLS", "1_38", 1, 38, "MLS", "Le Mol\u00e9son", 1, "MeteoSchweiz", 7.018, 46.546, 1977.0, 10.0, 13.31),
83+
("PAY", "1_32", 1, 32, "PAY", "Payerne", 1, "MeteoSchweiz", 6.942, 46.811, 489.17, 10.0, 0.0),
84+
("NAP", "1_48", 1, 48, "NAP", "Napf", 1, "MeteoSchweiz", 7.94, 47.005, 1404.03, 15.0, 0.0),
85+
86+
],
87+
columns=[
88+
"station",
89+
"name",
90+
"type_id",
91+
"point_id",
92+
"nat_abbr",
93+
"fullname",
94+
"owner_id",
95+
"owner_name",
96+
"longitude",
97+
"latitude",
98+
"height_masl",
99+
"pole_height",
100+
"roof_height",
101+
],
102+
)
103+
return (stations,)
104+
105+
106+
@app.cell
107+
def load_grib_data(data_source, grib_decoder, grib_dir, init_time, param):
108+
if param == "10sp":
109+
paramlist = ["10u", "10v"]
110+
elif param == "sp":
111+
paramlist = ["u", "v"]
112+
else:
113+
paramlist = [param]
114+
115+
grib_files = sorted(grib_dir.glob(f"{init_time}*.grib"))
116+
fds = data_source.FileDataSource(datafiles=grib_files)
117+
ds_grib = grib_decoder.load(fds, {"param": paramlist})
118+
ds_grib = ds_grib[param].squeeze()
119+
ds_grib
120+
return ds_grib, paramlist
121+
122+
123+
@app.cell
124+
def load_zarr_data(ds_grib, np, param, paramlist, xr, zarr_dir):
125+
ds_zarr = xr.open_zarr(zarr_dir, consolidated=False)
126+
ds_zarr = ds_zarr.set_index(time="dates")
127+
ds_zarr = ds_zarr.sel(time=ds_grib.valid_time.values)
128+
ds_zarr = ds_zarr.assign_coords({"variable": ds_zarr.attrs["variables"]})
129+
ds_zarr = ds_zarr.sel(variable=[p for p in paramlist]).squeeze("ensemble", drop=True)
130+
131+
# recover original 2D shape
132+
ny, nx = ds_zarr.attrs["field_shape"]
133+
y_idx, x_idx = np.unravel_index(np.arange(ny * nx), shape=(ny, nx))
134+
ds_zarr = ds_zarr.assign_coords({"y": ("cell", y_idx), "x": ("cell", x_idx)})
135+
ds_zarr = ds_zarr.set_index(cell=("y", "x"))
136+
ds_zarr = ds_zarr.unstack("cell")
137+
138+
# set lat lon as coords
139+
ds_zarr = ds_zarr.rename({"latitudes": "lat", "longitudes": "lon"})
140+
ds_zarr = ds_zarr.set_coords(["lat", "lon"])
141+
ds_zarr = ds_zarr.sel(variable=param).squeeze()["data"]
142+
ds_zarr
143+
return (ds_zarr,)
144+
145+
146+
@app.cell
147+
def _(ds_grib, ds_zarr, np, pd, stations):
148+
def nearest_yx_euclid(ds_grib, lat_s, lon_s):
149+
"""
150+
Return (y_idx, x_idx) of the grid point nearest to (lat_s, lon_s)
151+
using Euclidean distance in degrees.
152+
"""
153+
lat2d = ds_grib['lat'] # (y, x)
154+
lon2d = ds_grib['lon'] # (y, x)
155+
156+
dist2 = (lat2d - lat_s)**2 + (lon2d - lon_s)**2
157+
flat_idx = np.nanargmin(dist2.values)
158+
y_idx, x_idx = np.unravel_index(flat_idx, dist2.shape)
159+
return int(y_idx), int(x_idx)
160+
161+
def get_grib_idx_row(row):
162+
return nearest_yx_euclid(ds_grib, row['latitude'], row['longitude'])
163+
idxs_grib = stations.apply(get_grib_idx_row, axis=1, result_type='expand')
164+
idxs_grib.columns = ['grib_y_idx', 'grib_x_idx']
165+
166+
def get_zarr_idx_row(row):
167+
return nearest_yx_euclid(ds_zarr, row['latitude'], row['longitude'])
168+
idxs_zarr = stations.apply(get_zarr_idx_row, axis=1, result_type='expand')
169+
idxs_zarr.columns = ['zarr_y_idx', 'zarr_x_idx']
170+
171+
sta_idxs = pd.concat([stations, idxs_grib, idxs_zarr], axis=1).set_index("station")
172+
sta_idxs
173+
return (sta_idxs,)
174+
175+
176+
@app.cell
177+
def _(ds_grib, ds_zarr, outfn, plt, sta_idxs, station):
178+
grib_x_idx, grib_y_idx = sta_idxs.loc[station].grib_x_idx, sta_idxs.loc[station].grib_y_idx
179+
zarr_x_idx, zarr_y_idx = sta_idxs.loc[station].zarr_x_idx, sta_idxs.loc[station].zarr_y_idx
180+
181+
# analysis
182+
ds_zarr.isel(x=zarr_x_idx, y=zarr_y_idx).plot(x="time", label="analysis", color="k", ls="--")
183+
184+
# TODO: plot actual forecaster data...
185+
ds_grib.isel(x=grib_x_idx, y=grib_y_idx).isel(lead_time=list(range(0, 126, 6))).plot(x="valid_time", marker="o", linestyle='None', color="k", label="forecaster")
186+
187+
# interpolator
188+
ds_grib.isel(x=grib_x_idx, y=grib_y_idx).plot(x="valid_time", label="interpolator")
189+
190+
plt.legend()
191+
plt.ylabel(f"{ds_grib.attrs["parameter"]["shortName"]} ({ds_grib.attrs["parameter"]["units"]})")
192+
plt.title(f"{init_time} {ds_grib.attrs["parameter"]["name"]} at {station}")
193+
plt.savefig(outfn)
194+
return
195+
196+
197+
if __name__ == "__main__":
198+
app.run()

0 commit comments

Comments
 (0)