99consists of the nowcast(s). In between the start and end time, the nowcast(s)
1010weight decreases and NWP forecasts weight increases linearly from 1(0) to
11110(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.
1314
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.
1516
1617.. autosummary::
1718 :toctree: ../generated/
2223import numpy as np
2324from pysteps import nowcasts
2425from pysteps .utils import conversion
26+ from scipy .stats import rankdata
2527
2628
2729def forecast (
@@ -36,10 +38,11 @@ def forecast(
3638 start_blending = 120 ,
3739 end_blending = 240 ,
3840 fill_nwp = True ,
41+ saliency = False ,
3942 nowcast_kwargs = None ,
4043):
4144
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
4346
4447 Parameters
4548 ----------
@@ -81,12 +84,18 @@ def forecast(
8184 fill_nwp: bool, optional
8285 Standard value is True. If True, the NWP data will be used to fill in the
8386 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.
8492 nowcast_kwargs: dict, optional
8593 Dictionary containing keyword arguments for the nowcast method.
8694
95+
8796 Returns
8897 -------
89- R_blended : ndarray
98+ precip_blended : ndarray
9099 Array of shape (timesteps, m, n) in the case of no ensemble or
91100 of shape (n_ens_members, timesteps, m, n) in the case of an ensemble
92101 containing the precipation forecast generated by linearly blending
@@ -166,45 +175,139 @@ def forecast(
166175 )
167176
168177 # Initialise output
169- R_blended = np .zeros_like (precip_nowcast )
178+ precip_blended = np .zeros_like (precip_nowcast )
170179
171180 # Calculate the weights
172181 for i in range (timesteps ):
173182 # Calculate what time we are at
174183 t = (i + 1 ) * timestep
175184
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+
176194 # Calculate the weight with a linear relation (weight_nwp at start_blending = 0.0)
177195 # and (weight_nwp at end_blending = 1.0)
178196 weight_nwp = (t - start_blending ) / (end_blending - start_blending )
179197
180198 # Set weights at times before start_blending and after end_blending
181- if weight_nwp < 0.0 :
199+ if weight_nwp <= 0.0 :
182200 weight_nwp = 0.0
183- elif weight_nwp > 1.0 :
184- weight_nwp = 1.0
201+ precip_blended [slc_id ] = precip_nowcast [slc_id ]
185202
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 ]
188206
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- )
196207 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+ )
201227
202228 # Find where the NaN values are and replace them with NWP data
203229 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 ]
206232 else :
207233 # 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+
209282
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