Skip to content

Commit b12c420

Browse files
authored
Add first version of the reduced-space ensemble Kalman filter method as part of pySTEPS#464 (pySTEPS#491)
* Add first version of reduced-space EnKF combination technique
1 parent 96f7d10 commit b12c420

22 files changed

Lines changed: 3278 additions & 7 deletions

ci/ci_test_env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies:
2626
- PyWavelets
2727
- pandas
2828
- scikit-image
29+
- scikit-learn
2930
- rasterio
3031
- gdal
3132
# Test dependencies

doc/source/pysteps_reference/blending.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Implementation of blending methods for blending (ensemble) nowcasts with Numeric
66

77
.. automodule:: pysteps.blending.interface
88
.. automodule:: pysteps.blending.clim
9+
.. automodule:: pysteps.blending.ens_kalman_filter_blending
10+
.. automodule:: pysteps.blending.ens_kalman_filter_methods
911
.. automodule:: pysteps.blending.linear_blending
1012
.. automodule:: pysteps.blending.skill_scores
1113
.. automodule:: pysteps.blending.steps

doc/source/pysteps_reference/utils.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ Implementation of miscellaneous utility functions.
1313
.. automodule:: pysteps.utils.fft
1414
.. automodule:: pysteps.utils.images
1515
.. automodule:: pysteps.utils.interpolate
16+
.. automodule:: pysteps.utils.pca
17+
.. automodule:: pysteps.utils.reprojection
1618
.. automodule:: pysteps.utils.spectral
1719
.. automodule:: pysteps.utils.tapering
1820
.. automodule:: pysteps.utils.transformation
19-
.. automodule:: pysteps.utils.reprojection

doc/source/references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,14 @@ @ARTICLE{Imhoff2023
388388
YEAR = 2023,
389389
DOI = "10.1002/qj.4461"
390390
}
391+
392+
@ARTICLE{Nerini2019MWR,
393+
title = {A {Reduced}-{Space} {Ensemble} {Kalman} {Filter} {Approach} for {Flow}-{Dependent} {Integration} of {Radar} {Extrapolation} {Nowcasts} and {NWP} {Precipitation} {Ensembles}},
394+
volume = {147},
395+
doi = {10.1175/MWR-D-18-0258.1},
396+
number = {3},
397+
journal = {Monthly Weather Review},
398+
author = {D. Nerini and L. Foresti and D. Leuenberger and S. Robert and U. Germann},
399+
year = {2019},
400+
pages = {987--1006},
401+
}

doc/source/user_guide/example_data.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ directory structure looks like this::
7676
├── KNMI
7777
├── OPERA
7878
├── bom
79+
├── dwd
7980
├── fmi
8081
├── mch
8182

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ channels:
33
- conda-forge
44
- defaults
55
dependencies:
6-
- python>=3.11
6+
- python>=3.10
77
- jsmin
88
- jsonschema
99
- matplotlib

environment_dev.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ channels:
44
- conda-forge
55
- defaults
66
dependencies:
7-
- python>=3.11
7+
- python>=3.10
88
- pip
99
- jsmin
1010
- jsonschema
@@ -29,5 +29,6 @@ dependencies:
2929
- pre_commit
3030
- cartopy>=0.18
3131
- scikit-image
32+
- scikit-learn
3233
- pandas
3334
- rasterio
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Blended forecast
4+
====================
5+
6+
This tutorial shows how to construct a blended forecast between an ensemble nowcast
7+
and an ensemble Numerical Weather Prediction (NWP) rainfall forecast using the
8+
Reduced-Space Ensemble Kalman filter approach in :cite:`Nerini2019MWR`. The used
9+
datasets are from the German Weather Service (DWD).
10+
11+
"""
12+
13+
import os
14+
from datetime import datetime, timedelta
15+
16+
import numpy as np
17+
from matplotlib import pyplot as plt
18+
19+
import pysteps
20+
from pysteps import io, rcparams, blending
21+
from pysteps.visualization import plot_precip_field
22+
import pysteps_nwp_importers
23+
24+
25+
################################################################################
26+
# Read the radar images and the NWP forecast
27+
# ------------------------------------------
28+
#
29+
# First, we import a sequence of 4 images of 5-minute radar composites
30+
# and the corresponding NWP rainfall forecast that was available at that time.
31+
#
32+
# You need the pysteps-data archive downloaded and the pystepsrc file
33+
# configured with the data_source paths pointing to data folders.
34+
# Additionally, the pysteps-nwp-importers plugin needs to be installed, see
35+
# https://github.com/pySTEPS/pysteps-nwp-importers.
36+
37+
# Selected case
38+
date_radar = datetime.strptime("202506041645", "%Y%m%d%H%M")
39+
# The last NWP forecast was issued at 16:00 - the blending tool will be able
40+
# to find the correct lead times itself.
41+
date_nwp = datetime.strptime("202506041600", "%Y%m%d%H%M")
42+
radar_data_source = rcparams.data_sources["dwd"]
43+
nwp_data_source = rcparams.data_sources["dwd_nwp"]
44+
45+
46+
###############################################################################
47+
# Load the data from the archive
48+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
49+
50+
root_path = radar_data_source["root_path"]
51+
path_fmt = radar_data_source["path_fmt"]
52+
fn_pattern = radar_data_source["fn_pattern"]
53+
fn_ext = radar_data_source["fn_ext"]
54+
importer_name = radar_data_source["importer"]
55+
importer_kwargs = radar_data_source["importer_kwargs"]
56+
timestep_radar = radar_data_source["timestep"]
57+
58+
# Find the radar files in the archive
59+
fns = io.find_by_date(
60+
date_radar,
61+
root_path,
62+
path_fmt,
63+
fn_pattern,
64+
fn_ext,
65+
timestep_radar,
66+
num_prev_files=2,
67+
)
68+
69+
# Read the radar composites (which are already in mm/h)
70+
importer = io.get_method(importer_name, "importer")
71+
radar_precip, _, radar_metadata = io.read_timeseries(fns, importer, **importer_kwargs)
72+
73+
# Import the NWP data
74+
filename = os.path.join(
75+
nwp_data_source["root_path"],
76+
datetime.strftime(date_nwp, nwp_data_source["path_fmt"]),
77+
datetime.strftime(date_nwp, nwp_data_source["fn_pattern"])
78+
+ "."
79+
+ nwp_data_source["fn_ext"],
80+
)
81+
nwp_importer = io.get_method("dwd_nwp", "importer")
82+
kwargs = {
83+
"varname": "lsprate",
84+
"grid_file_path": "./aux/grid_files/dwd/icon/R19B07/icon_grid_0047_R19B07_L.nc",
85+
}
86+
nwp_precip, _, nwp_metadata = nwp_importer(filename, **kwargs)
87+
88+
89+
################################################################################
90+
# Pre-processing steps
91+
# --------------------
92+
93+
# Set the zerovalue and precipitation thresholds (these are fixed from DWD)
94+
prec_thr = 0.049
95+
zerovalue = 0.027
96+
97+
# Transform the zerovalue and precipitation thresholds to dBR
98+
log_thr_prec = 10.0 * np.log10(prec_thr)
99+
log_zerovalue = 10.0 * np.log10(zerovalue)
100+
101+
# Reproject the DWD ICON NWP data onto a regular grid
102+
nwp_metadata["clon"] = nwp_precip["longitude"].values
103+
nwp_metadata["clat"] = nwp_precip["latitude"].values
104+
# We change the time step from the DWD NWP data to 15 min (it is actually 5 min)
105+
# to have a longer forecast horizon available for this example, as pysteps_data
106+
# only contains 1 hour of DWD forecast data (to minimize storage).
107+
nwp_metadata["accutime"] = 15.0
108+
nwp_precip = (
109+
nwp_precip.values * 3.0
110+
) # (to account for the change in time step from 5 to 15 min)
111+
112+
# Reproject ID2 data onto a regular grid
113+
nwp_precip_rprj, nwp_metadata_rprj = (
114+
pysteps_nwp_importers.importer_dwd_nwp.unstructured2regular(
115+
nwp_precip, nwp_metadata, radar_metadata
116+
)
117+
)
118+
119+
# Make sure the units are in mm/h
120+
converter = pysteps.utils.get_method("mm/h")
121+
radar_precip, radar_metadata = converter(
122+
radar_precip, radar_metadata
123+
) # The radar data should already be in mm/h
124+
nwp_precip_rprj, nwp_metadata_rprj = converter(nwp_precip_rprj, nwp_metadata_rprj)
125+
126+
# Threshold the data
127+
radar_precip[radar_precip < prec_thr] = 0.0
128+
nwp_precip_rprj[nwp_precip_rprj < prec_thr] = 0.0
129+
130+
# Plot the radar rainfall field and the first time step and first ensemble member
131+
# of the NWP forecast.
132+
date_str = datetime.strftime(date_radar, "%Y-%m-%d %H:%M")
133+
plt.figure(figsize=(10, 5))
134+
plt.subplot(121)
135+
plot_precip_field(
136+
radar_precip[-1, :, :],
137+
geodata=radar_metadata,
138+
title=f"Radar observation at {date_str}",
139+
colorscale="STEPS-NL",
140+
)
141+
plt.subplot(122)
142+
plot_precip_field(
143+
nwp_precip_rprj[0, 0, :, :],
144+
geodata=nwp_metadata_rprj,
145+
title=f"NWP forecast at {date_str}",
146+
colorscale="STEPS-NL",
147+
)
148+
plt.tight_layout()
149+
plt.show()
150+
151+
# transform the data to dB
152+
transformer = pysteps.utils.get_method("dB")
153+
radar_precip, radar_metadata = transformer(
154+
radar_precip, radar_metadata, threshold=prec_thr, zerovalue=log_zerovalue
155+
)
156+
nwp_precip_rprj, nwp_metadata_rprj = transformer(
157+
nwp_precip_rprj, nwp_metadata_rprj, threshold=prec_thr, zerovalue=log_zerovalue
158+
)
159+
160+
161+
###############################################################################
162+
# Determine the velocity fields
163+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
164+
#
165+
# In contrast to the STEPS blending method, no motion field for the NWP fields
166+
# is needed in the ensemble kalman filter blending approach.
167+
168+
# Estimate the motion vector field
169+
oflow_method = pysteps.motion.get_method("lucaskanade")
170+
velocity_radar = oflow_method(radar_precip)
171+
172+
173+
################################################################################
174+
# The blended forecast
175+
# ~~~~~~~~~~~~~~~~~~~~
176+
177+
# Set the timestamps for radar_precip and nwp_precip_rprj
178+
timestamps_radar = np.array(
179+
sorted(
180+
[
181+
date_radar - timedelta(minutes=i * timestep_radar)
182+
for i in range(len(radar_precip))
183+
]
184+
)
185+
)
186+
timestamps_nwp = np.array(
187+
sorted(
188+
[
189+
date_nwp + timedelta(minutes=i * int(nwp_metadata_rprj["accutime"]))
190+
for i in range(nwp_precip_rprj.shape[0])
191+
]
192+
)
193+
)
194+
195+
# Set the combination kwargs
196+
combination_kwargs = dict(
197+
n_tapering=0, # No. of principal components of the ens. forecast for which the cov. matrix is computed
198+
non_precip_mask=True, # Specifies whether the computation should be truncated on grid boxes where at least a minimum number of ens. members forecast precipitation.
199+
n_ens_prec=1, # Minimum number of ens. members that forecast precip for the above-mentioned mask.
200+
lien_criterion=True, # Specifies wheter the Lien criterion should be applied.
201+
n_lien=10, # Minimum number of ensemble members that forecast precipitation for the Lien criterion (equals half the ens. members here)
202+
prob_matching="iterative", # The type of probability matching used.
203+
inflation_factor_bg=1.8, # Inflation factor of the background (NWC) covariance matrix. (this value indicates a faster convergence towards the NWP ensemble)
204+
inflation_factor_obs=1.0, # Inflation factor of the observation (NWP) covariance matrix.
205+
offset_bg=0.0, # Offset of the background (NWC) covariance matrix.
206+
offset_obs=0.0, # Offset of the observation (NWP) covariance matrix.
207+
nwp_hres_eff=14.0, # Effective horizontal resolution of the utilized NWP model (in km here).
208+
sampling_prob_source="ensemble", # Computation method of the sampling probability for the probability matching. 'ensemble' computes this probability as the ratio between the ensemble differences.
209+
use_accum_sampling_prob=False, # Specifies whether the current sampling probability should be used for the probability matching or a probability integrated over the previous forecast time.
210+
)
211+
212+
213+
# Call the PCA EnKF method
214+
blending_method = blending.get_method("pca_enkf")
215+
precip_forecast = blending_method(
216+
obs_precip=radar_precip, # Radar data in dBR
217+
obs_timestamps=timestamps_radar, # Radar timestamps
218+
nwp_precip=nwp_precip_rprj, # NWP in dBR
219+
nwp_timestamps=timestamps_nwp, # NWP timestamps
220+
velocity=velocity_radar, # Velocity vector field
221+
forecast_horizon=120, # Forecast length (horizon) in minutes - only a short forecast horizon due to the limited dataset length stored here.
222+
issuetime=date_radar, # Forecast issue time as datetime object
223+
n_ens_members=20, # No. of ensemble members
224+
precip_mask_dilation=1, # Dilation of precipitation mask in grid boxes
225+
n_cascade_levels=6, # No. of cascade levels
226+
precip_thr=log_thr_prec, # Precip threshold
227+
norain_thr=0.0005, # Minimum of 0.5% precip needed, otherwise 'zero rainfall'
228+
num_workers=4, # No. of parallel threads
229+
noise_stddev_adj="auto", # Standard deviation adjustment
230+
noise_method="ssft", # SSFT as noise method
231+
enable_combination=True, # Enable combination
232+
noise_kwargs={"win_size": (512, 512), "win_fun": "hann", "overlap": 0.5},
233+
extrap_kwargs={"interp_order": 3, "map_coordinates_mode": "nearest"},
234+
combination_kwargs=combination_kwargs,
235+
filter_kwargs={"include_mean": True},
236+
)
237+
238+
# Transform the data back into mm/h
239+
precip_forecast, _ = converter(precip_forecast, radar_metadata)
240+
radar_precip, _ = converter(radar_precip, radar_metadata)
241+
nwp_precip, _ = converter(nwp_precip_rprj, nwp_metadata_rprj)
242+
243+
244+
################################################################################
245+
# Visualize the output
246+
# ~~~~~~~~~~~~~~~~~~~~
247+
#
248+
# The NWP rainfall forecast has a much lower weight than the radar-based
249+
# extrapolation # forecast at the issue time of the forecast (+0 min). Therefore,
250+
# the first time steps consist mostly of the extrapolation. However, near the end
251+
# of the forecast (+180 min), the NWP share in the blended forecast has become
252+
# the more dominant contribution to the forecast and thus the forecast starts
253+
# to resemble the NWP forecast.
254+
255+
fig = plt.figure(figsize=(5, 12))
256+
257+
leadtimes_min = [15, 30, 45, 60, 90, 120]
258+
n_leadtimes = len(leadtimes_min)
259+
for n, leadtime in enumerate(leadtimes_min):
260+
# Nowcast with blending into NWP
261+
plt.subplot(n_leadtimes, 2, n * 2 + 1)
262+
plot_precip_field(
263+
precip_forecast[0, int(leadtime / timestep_radar) - 1, :, :],
264+
geodata=radar_metadata,
265+
title=f"Blended +{leadtime} min",
266+
axis="off",
267+
colorscale="STEPS-NL",
268+
colorbar=False,
269+
)
270+
271+
# Raw NWP forecast
272+
plt.subplot(n_leadtimes, 2, n * 2 + 2)
273+
plot_precip_field(
274+
nwp_precip[int(leadtime / int(nwp_metadata_rprj["accutime"])) - 1, 0, :, :],
275+
geodata=nwp_metadata_rprj,
276+
title=f"NWP +{leadtime} min",
277+
axis="off",
278+
colorscale="STEPS-NL",
279+
colorbar=False,
280+
)
281+
282+
283+
################################################################################
284+
# References
285+
# ~~~~~~~~~~
286+
#
287+
288+
# Nerini, D., Foresti, L., Leuenberger, D., Robert, S., Germann, U. 2019. "A
289+
# Reduced-Space Ensemble Kalman Filter Approach for Flow-Dependent Integration
290+
# of Radar Extrapolation Nowcasts and NWP Precipitation Ensembles." Monthly
291+
# Weather Review 147(3): 987-1006. https://doi.org/10.1175/MWR-D-18-0258.1.

0 commit comments

Comments
 (0)