9
9
consists of the nowcast(s). In between the start and end time, the nowcast(s)
10
10
weight decreases and NWP forecasts weight increases linearly from 1(0) to
11
11
0(1). After the end time, the blended forecast entirely consists of the NWP
12
- forecasts.
12
+ forecasts. The saliency-based blending method also takes into account the pixel
13
+ intensities and preserves them if they are strong enough based on their ranked salience.
13
14
14
- Implementation of the linear blending between nowcast and NWP data.
15
+ Implementation of the linear blending and saliency-based blending between nowcast and NWP data.
15
16
16
17
.. autosummary::
17
18
:toctree: ../generated/
22
23
import numpy as np
23
24
from pysteps import nowcasts
24
25
from pysteps .utils import conversion
26
+ from scipy .stats import rankdata
25
27
26
28
27
29
def forecast (
@@ -36,10 +38,11 @@ def forecast(
36
38
start_blending = 120 ,
37
39
end_blending = 240 ,
38
40
fill_nwp = True ,
41
+ saliency = False ,
39
42
nowcast_kwargs = None ,
40
43
):
41
44
42
- """Generate a forecast by linearly blending nowcasts with NWP data
45
+ """Generate a forecast by linearly or saliency-based blending of nowcasts with NWP data
43
46
44
47
Parameters
45
48
----------
@@ -81,12 +84,18 @@ def forecast(
81
84
fill_nwp: bool, optional
82
85
Standard value is True. If True, the NWP data will be used to fill in the
83
86
no data mask of the nowcast.
87
+ saliency: bool, optional
88
+ Default value is False. If True, saliency will be used for blending. The blending
89
+ is based on intensities and forecast times as described in :cite:`Hwang2015`. The blended
90
+ product preserves pixel intensities with time if they are strong enough based on their ranked
91
+ salience.
84
92
nowcast_kwargs: dict, optional
85
93
Dictionary containing keyword arguments for the nowcast method.
86
94
95
+
87
96
Returns
88
97
-------
89
- R_blended : ndarray
98
+ precip_blended : ndarray
90
99
Array of shape (timesteps, m, n) in the case of no ensemble or
91
100
of shape (n_ens_members, timesteps, m, n) in the case of an ensemble
92
101
containing the precipation forecast generated by linearly blending
@@ -166,45 +175,139 @@ def forecast(
166
175
)
167
176
168
177
# Initialise output
169
- R_blended = np .zeros_like (precip_nowcast )
178
+ precip_blended = np .zeros_like (precip_nowcast )
170
179
171
180
# Calculate the weights
172
181
for i in range (timesteps ):
173
182
# Calculate what time we are at
174
183
t = (i + 1 ) * timestep
175
184
185
+ if n_ens_members_max == 1 :
186
+ ref_dim = 0
187
+ else :
188
+ ref_dim = 1
189
+
190
+ # apply blending
191
+ # compute the slice indices
192
+ slc_id = _get_slice (precip_blended .ndim , ref_dim , i )
193
+
176
194
# Calculate the weight with a linear relation (weight_nwp at start_blending = 0.0)
177
195
# and (weight_nwp at end_blending = 1.0)
178
196
weight_nwp = (t - start_blending ) / (end_blending - start_blending )
179
197
180
198
# Set weights at times before start_blending and after end_blending
181
- if weight_nwp < 0.0 :
199
+ if weight_nwp <= 0.0 :
182
200
weight_nwp = 0.0
183
- elif weight_nwp > 1.0 :
184
- weight_nwp = 1.0
201
+ precip_blended [slc_id ] = precip_nowcast [slc_id ]
185
202
186
- # Calculate weight_nowcast
187
- weight_nowcast = 1.0 - weight_nwp
203
+ elif weight_nwp >= 1.0 :
204
+ weight_nwp = 1.0
205
+ precip_blended [slc_id ] = precip_nwp [slc_id ]
188
206
189
- # Calculate output by combining precip_nwp and precip_nowcast,
190
- # while distinguishing between ensemble and non-ensemble methods
191
- if n_ens_members_max == 1 :
192
- R_blended [i , :, :] = (
193
- weight_nwp * precip_nwp [i , :, :]
194
- + weight_nowcast * precip_nowcast [i , :, :]
195
- )
196
207
else :
197
- R_blended [:, i , :, :] = (
198
- weight_nwp * precip_nwp [:, i , :, :]
199
- + weight_nowcast * precip_nowcast [:, i , :, :]
200
- )
208
+ # Calculate weight_nowcast
209
+ weight_nowcast = 1.0 - weight_nwp
210
+
211
+ # Calculate output by combining precip_nwp and precip_nowcast,
212
+ # while distinguishing between ensemble and non-ensemble methods
213
+ if saliency :
214
+ ranked_salience = _get_ranked_salience (
215
+ precip_nowcast [slc_id ], precip_nwp [slc_id ]
216
+ )
217
+ ws = _get_ws (weight_nowcast , ranked_salience )
218
+ precip_blended [slc_id ] = (
219
+ ws * precip_nowcast [slc_id ] + (1 - ws ) * precip_nwp [slc_id ]
220
+ )
221
+
222
+ else :
223
+ precip_blended [slc_id ] = (
224
+ weight_nwp * precip_nwp [slc_id ]
225
+ + weight_nowcast * precip_nowcast [slc_id ]
226
+ )
201
227
202
228
# Find where the NaN values are and replace them with NWP data
203
229
if fill_nwp :
204
- nan_indices = np .isnan (R_blended )
205
- R_blended [nan_indices ] = precip_nwp [nan_indices ]
230
+ nan_indices = np .isnan (precip_blended )
231
+ precip_blended [nan_indices ] = precip_nwp [nan_indices ]
206
232
else :
207
233
# If no NWP data is given, the blended field is simply equal to the nowcast field
208
- R_blended = precip_nowcast
234
+ precip_blended = precip_nowcast
235
+
236
+ return precip_blended
237
+
238
+
239
+ def _get_slice (n_dims , ref_dim , ref_id ):
240
+ """source: https://stackoverflow.com/a/24399139/4222370"""
241
+ slc = [slice (None )] * n_dims
242
+ slc [ref_dim ] = ref_id
243
+ return tuple (slc )
244
+
245
+
246
+ def _get_ranked_salience (precip_nowcast , precip_nwp ):
247
+ """Calculate ranked salience, which show how close the pixel is to the maximum intensity difference [r(x,y)=1]
248
+ or the minimum intensity difference [r(x,y)=0]
249
+
250
+ Parameters
251
+ ----------
252
+ precip_nowcast: array_like
253
+ Array of shape (m,n) containing the extrapolated precipitation field at a specified timestep
254
+ precip_nwp: array_like
255
+ Array of shape (m,n) containing the NWP fields at a specified timestep
256
+
257
+ Returns
258
+ -------
259
+ ranked_salience:
260
+ Array of shape (m,n) containing ranked salience
261
+ """
262
+
263
+ # calcutate normalized intensity
264
+ if np .max (precip_nowcast ) == 0 :
265
+ norm_nowcast = np .zeros_like (precip_nowcast )
266
+ else :
267
+ norm_nowcast = precip_nowcast / np .max (precip_nowcast )
268
+
269
+ if np .max (precip_nwp ) == 0 :
270
+ norm_nwp = np .zeros_like (precip_nwp )
271
+ else :
272
+ norm_nwp = precip_nwp / np .max (precip_nwp )
273
+
274
+ diff = norm_nowcast - norm_nwp
275
+
276
+ # Calculate ranked salience, based on dense ranking method, in which equally comparable values receive the same ranking number
277
+ ranked_salience = rankdata (diff , method = "dense" ).reshape (diff .shape ).astype ("float" )
278
+ ranked_salience /= ranked_salience .max ()
279
+
280
+ return ranked_salience
281
+
209
282
210
- return R_blended
283
+ def _get_ws (weight , ranked_salience ):
284
+ """Calculate salience weight based on linear weight and ranked salience as described in :cite:`Hwang2015`.
285
+ Cells with higher intensities result in larger weights.
286
+
287
+ Parameters
288
+ ----------
289
+ weight: int
290
+ Varying between 0 and 1
291
+ ranked_salience: array_like
292
+ Array of shape (m,n) containing ranked salience
293
+
294
+ Returns
295
+ -------
296
+ ws: array_like
297
+ Array of shape (m,n) containing salience weight, which preserves pixel intensties with time if they are strong
298
+ enough based on the ranked salience.
299
+ """
300
+
301
+ # Calculate salience weighte
302
+ ws = 0.5 * (
303
+ (weight * ranked_salience )
304
+ / (weight * ranked_salience + (1 - weight ) * (1 - ranked_salience ))
305
+ + (
306
+ np .sqrt (ranked_salience ** 2 + weight ** 2 )
307
+ / (
308
+ np .sqrt (ranked_salience ** 2 + weight ** 2 )
309
+ + np .sqrt ((1 - ranked_salience ) ** 2 + (1 - weight ) ** 2 )
310
+ )
311
+ )
312
+ )
313
+ return ws
0 commit comments