Skip to content

Commit fb8f65c

Browse files
NathalieRombeekNathalie Rombeek
andauthored
Implement option for saliency-based blending (#285)
* Implement option for salience weight * Remove code duplication and add context * extend tests, add example and change docstring * Improv example a Co-authored-by: Nathalie Rombeek <nathalie.rombeek@meteoswiss.ch>
1 parent d9648c4 commit fb8f65c

File tree

5 files changed

+301
-67
lines changed

5 files changed

+301
-67
lines changed

doc/source/references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,17 @@ @ARTICLE{Her2000
136136
DOI = "10.1175/1520-0434(2000)015<0559:DOTCRP>2.0.CO;2"
137137
}
138138

139+
@article{Hwang2015,
140+
AUTHOR = "Hwang, Yunsung and Clark, Adam J and Lakshmanan, Valliappa and Koch, Steven E",
141+
TITLE = "Improved nowcasts by blending extrapolation and model forecasts",
142+
JOURNAL = "Weather and Forecasting",
143+
VOLUME = 30,
144+
NUMBER = 5,
145+
PAGES = "1201--1217",
146+
YEAR = 2015,
147+
DOI = "10.1175/WAF-D-15-0057.1"
148+
}
149+
139150
@ARTICLE{NBSG2017,
140151
AUTHOR = "D. Nerini and N. Besic and I. Sideris and U. Germann and L. Foresti",
141152
TITLE = "A non-stationary stochastic ensemble generator for radar rainfall fields based on the short-space {F}ourier transform",

examples/plot_linear_blending.py

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from matplotlib import pyplot as plt
1616

1717
import pysteps
18-
from pysteps import io, rcparams, blending
18+
from pysteps import io, rcparams, nowcasts, blending
19+
from pysteps.utils import conversion
1920
from pysteps.visualization import plot_precip_field
2021

2122

@@ -149,41 +150,110 @@
149150

150151

151152
################################################################################
152-
# Visualize the output
153-
# ~~~~~~~~~~~~~~~~~~~~
153+
# The salient blending of nowcast and NWP rainfall forecast
154+
# ---------------------------------------------------------
154155
#
156+
# This method follows the saliency-based blending procedure described in :cite:`Hwang2015`. The
157+
# blending is based on intensities and forecast times. The blended product preserves pixel
158+
# intensities with time if they are strong enough based on their ranked salience. Saliency is
159+
# the property of an object to be outstanding with respect to its surroundings. The ranked salience
160+
# is calculated by first determining the difference in the normalized intensity of the nowcasts
161+
# and NWP. Next, the pixel intensities are ranked, in which equally comparable values receive
162+
# the same ranking number.
163+
164+
# Calculate the salient blended precipitation field
165+
precip_salient_blended = blending.linear_blending.forecast(
166+
precip=radar_precip[-1, :, :],
167+
precip_metadata=radar_metadata,
168+
velocity=velocity_radar,
169+
timesteps=18,
170+
timestep=10,
171+
nowcast_method="extrapolation", # simple advection nowcast
172+
precip_nwp=precip_nwp,
173+
precip_nwp_metadata=nwp_metadata,
174+
start_blending=60, # in minutes (this is an arbritrary choice)
175+
end_blending=120, # in minutes (this is an arbritrary choice)
176+
saliency=True,
177+
)
178+
179+
180+
################################################################################
181+
# Visualize the output
182+
# --------------------
183+
184+
################################################################################
185+
# Calculate the radar rainfall nowcasts for visualization
186+
187+
nowcast_method_func = nowcasts.get_method("extrapolation")
188+
precip_nowcast = nowcast_method_func(
189+
precip=radar_precip[-1, :, :],
190+
velocity=velocity_radar,
191+
timesteps=18,
192+
)
193+
194+
# Make sure that precip_nowcast are in mm/h
195+
precip_nowcast, _ = conversion.to_rainrate(precip_nowcast, metadata=radar_metadata)
196+
197+
################################################################################
155198
# The linear blending starts at 60 min, so during the first 60 minutes the
156199
# blended forecast only consists of the extrapolation forecast (consisting of an
157200
# extrapolation nowcast). Between 60 and 120 min, the NWP forecast gradually gets more
158-
# weight, whereas the extrapolation forecasts gradually gets less weight.
159-
# After 120 min, the blended forecast entirely consists of the NWP rainfall
160-
# forecast.
201+
# weight, whereas the extrapolation forecasts gradually gets less weight. In addition,
202+
# the saliency-based blending takes also the difference in pixel intensities into account,
203+
# which are preserved over time if they are strong enough based on their ranked salience.
204+
# Furthermore, pixels with relative low intensities get a lower weight and stay smaller in
205+
# the saliency-based blending compared to linear blending. After 120 min, the blended
206+
# forecast entirely consists of the NWP rainfall forecast.
161207

162-
fig = plt.figure(figsize=(4, 8))
208+
fig = plt.figure(figsize=(8, 12))
163209

164-
leadtimes_min = [30, 60, 90, 120]
210+
leadtimes_min = [30, 60, 80, 100, 120]
165211
n_leadtimes = len(leadtimes_min)
166212
for n, leadtime in enumerate(leadtimes_min):
167213

214+
# Extrapolation
215+
plt.subplot(n_leadtimes, 4, n * 4 + 1)
216+
plot_precip_field(
217+
precip_nowcast[int(leadtime / timestep) - 1, :, :],
218+
geodata=radar_metadata,
219+
title=f"Nowcast + {leadtime} min",
220+
axis="off",
221+
colorbar=False,
222+
)
223+
168224
# Nowcast with blending into NWP
169-
plt.subplot(n_leadtimes, 2, n * 2 + 1)
225+
plt.subplot(n_leadtimes, 4, n * 4 + 2)
170226
plot_precip_field(
171227
precip_blended[int(leadtime / timestep) - 1, :, :],
172228
geodata=radar_metadata,
173-
title=f"Nowcast +{leadtime} min",
229+
title=f"Linear + {leadtime} min",
230+
axis="off",
231+
colorbar=False,
232+
)
233+
234+
# Nowcast with salient blending into NWP
235+
plt.subplot(n_leadtimes, 4, n * 4 + 3)
236+
plot_precip_field(
237+
precip_salient_blended[int(leadtime / timestep) - 1, :, :],
238+
geodata=radar_metadata,
239+
title=f"Salient + {leadtime} min",
174240
axis="off",
175241
colorbar=False,
176242
)
177243

178244
# Raw NWP forecast
179-
plt.subplot(n_leadtimes, 2, n * 2 + 2)
245+
plt.subplot(n_leadtimes, 4, n * 4 + 4)
180246
plot_precip_field(
181247
precip_nwp[int(leadtime / timestep) - 1, :, :],
182248
geodata=nwp_metadata,
183-
title=f"NWP +{leadtime} min",
249+
title=f"NWP + {leadtime} min",
184250
axis="off",
185251
colorbar=False,
186252
)
187253

188254
plt.tight_layout()
189255
plt.show()
256+
257+
################################################################################
258+
# Note that the NaN values of the extrapolation forecast are replaced with NWP data
259+
# in the blended forecast, even before the blending starts.

pysteps/blending/interface.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
get_method
1212
"""
1313

14+
from functools import partial
15+
1416
from pysteps.blending import linear_blending
1517
from pysteps.blending import steps
1618

1719
_blending_methods = dict()
1820
_blending_methods["linear_blending"] = linear_blending.forecast
21+
_blending_methods["salient_blending"] = partial(linear_blending.forecast, saliency=True)
1922
_blending_methods["steps"] = steps.forecast
2023

2124

@@ -32,6 +35,13 @@ def get_method(name):
3235
| linear_blending | the linear blending of a nowcast method with other |
3336
| | data (e.g. NWP data). |
3437
+------------------+------------------------------------------------------+
38+
| salient_blending | the salient blending of a nowcast method with other |
39+
| | data (e.g. NWP data) described in :cite:`Hwang2015`. |
40+
| | The blending is based on intensities and forecast |
41+
| | times. The blended product preserves pixel |
42+
| | intensities with time if they are strong enough based|
43+
| | on their ranked salience. |
44+
+------------------+------------------------------------------------------+
3545
| steps | the STEPS stochastic nowcasting blending method |
3646
| | described in :cite:`Seed2003`, :cite:`BPS2006` and |
3747
| | :cite:`SPN2013`. The blending weights approach |

pysteps/blending/linear_blending.py

Lines changed: 128 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
consists of the nowcast(s). In between the start and end time, the nowcast(s)
1010
weight decreases and NWP forecasts weight increases linearly from 1(0) to
1111
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.
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/
@@ -22,6 +23,7 @@
2223
import numpy as np
2324
from pysteps import nowcasts
2425
from pysteps.utils import conversion
26+
from scipy.stats import rankdata
2527

2628

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

Comments
 (0)