|
| 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