Skip to content

Commit 769880c

Browse files
RubenImhoffladc
andauthored
Allow for semi dynamic incremental mask in blending setup (#459)
* feat: allow for semi dynamic incremental mask in blending setup * fix: remove remnant code * Simplify max_rim range computation * fix: add tests and update docstrings --------- Co-authored-by: Lesley De Cruz <lesley.decruz@meteo.be>
1 parent 5fd4dc8 commit 769880c

File tree

2 files changed

+78
-58
lines changed

2 files changed

+78
-58
lines changed

pysteps/blending/steps.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ class StepsBlendingConfig:
147147
field, 'incremental' = iteratively buffer the mask with a certain rate
148148
(currently it is 1 km/min), None=no masking.
149149
resample_distribution: bool, optional
150-
Method to resample the distribution from the extrapolation and NWP cascade as input
151-
for the probability matching. Not resampling these distributions may lead to losing
152-
some extremes when the weight of both the extrapolation and NWP cascade is similar.
153-
Defaults to True.
150+
Method to resample the distribution from the extrapolation and NWP cascade as input
151+
for the probability matching. Not resampling these distributions may lead to losing
152+
some extremes when the weight of both the extrapolation and NWP cascade is similar.
153+
Defaults to True.
154154
smooth_radar_mask_range: int, Default is 0.
155155
Method to smooth the transition between the radar-NWP-noise blend and the NWP-noise
156156
blend near the edge of the radar domain (radar mask), where the radar data is either
@@ -249,9 +249,9 @@ class StepsBlendingConfig:
249249
(the number of NWP models) and 'window_length' (the minimum number of
250250
days the clim file should have, otherwise the default is used).
251251
mask_kwargs: dict
252-
Optional dictionary containing mask keyword arguments 'mask_f' and
253-
'mask_rim', the factor defining the the mask increment and the rim size,
254-
respectively.
252+
Optional dictionary containing mask keyword arguments 'mask_f',
253+
'mask_rim' and 'max_mask_rim', the factor defining the the mask
254+
increment and the (maximum) rim size, respectively.
255255
The mask increment is defined as mask_f*timestep/kmperpixel.
256256
measure_time: bool
257257
If set to True, measure, print and return the computation time.
@@ -639,7 +639,10 @@ def worker(j):
639639
self.__recompose_cascade_to_rainfall_field(j, worker_state)
640640
final_blended_forecast_single_member = (
641641
self.__post_process_output(
642-
j, final_blended_forecast_single_member, worker_state
642+
j,
643+
t_sub,
644+
final_blended_forecast_single_member,
645+
worker_state,
643646
)
644647
)
645648
final_blended_forecast_all_members_one_timestep[j] = (
@@ -1480,6 +1483,9 @@ def __prepare_forecast_loop(self):
14801483
if self.__config.mask_method == "incremental":
14811484
# get mask parameters
14821485
self.__params.mask_rim = self.__params.mask_kwargs.get("mask_rim", 10)
1486+
self.__params.max_mask_rim = self.__params.mask_kwargs.get(
1487+
"max_mask_rim", 10
1488+
)
14831489
mask_f = self.__params.mask_kwargs.get("mask_f", 1.0)
14841490
# initialize the structuring element
14851491
struct = generate_binary_structure(2, 1)
@@ -2511,7 +2517,7 @@ def __recompose_cascade_to_rainfall_field(self, j, worker_state):
25112517
)
25122518

25132519
def __post_process_output(
2514-
self, j, final_blended_forecast_single_member, worker_state
2520+
self, j, t_sub, final_blended_forecast_single_member, worker_state
25152521
):
25162522
"""
25172523
Apply post-processing steps to refine the final blended forecast. This
@@ -2674,16 +2680,16 @@ def __post_process_output(
26742680
worker_state.final_blended_forecast_recomposed.min()
26752681
)
26762682
if self.__config.mask_method == "incremental":
2677-
# The incremental mask is slightly different from
2678-
# the implementation in the non-blended steps.py, as
2679-
# it is not based on the last forecast, but instead
2680-
# on R_pm_blended. Therefore, the buffer does not
2681-
# increase over time.
2682-
# Get the mask for this forecast
2683+
# The incremental mask is slightly different from the implementation in
2684+
# nowcasts.steps.py, as it is not computed in the Lagrangian space. Instead,
2685+
# we use precip_forecast_probability_matched and let the mask_rim increase with
2686+
# the time step until mask_rim_max. This ensures that for the first t time
2687+
# steps, the buffer mask keeps increasing.
26832688
precip_field_mask = (
26842689
precip_forecast_probability_matching_blended
26852690
>= self.__config.precip_threshold
26862691
)
2692+
26872693
# Buffer the mask
26882694
# Convert the precipitation field mask into an 8-bit unsigned integer mask
26892695
obs_mask_uint8 = precip_field_mask.astype("uint8")
@@ -2698,7 +2704,10 @@ def __post_process_output(
26982704
accumulated_mask = dilated_mask.astype(float)
26992705

27002706
# Iteratively dilate the mask and accumulate the results to create a grayscale rim
2701-
for _ in range(self.__params.mask_rim):
2707+
mask_rim_temp = min(
2708+
self.__params.mask_rim + t_sub - 1, self.__params.max_mask_rim
2709+
)
2710+
for _ in range(mask_rim_temp):
27022711
dilated_mask = binary_dilation(dilated_mask, struct_element)
27032712
accumulated_mask += dilated_mask
27042713

@@ -3095,9 +3104,9 @@ def forecast(
30953104
(the number of NWP models) and 'window_length' (the minimum number of
30963105
days the clim file should have, otherwise the default is used).
30973106
mask_kwargs: dict
3098-
Optional dictionary containing mask keyword arguments 'mask_f' and
3099-
'mask_rim', the factor defining the the mask increment and the rim size,
3100-
respectively.
3107+
Optional dictionary containing mask keyword arguments 'mask_f',
3108+
'mask_rim' and 'max_mask_rim', the factor defining the the mask
3109+
increment and the (maximum) rim size, respectively.
31013110
The mask increment is defined as mask_f*timestep/kmperpixel.
31023111
measure_time: bool
31033112
If set to True, measure, print and return the computation time.

pysteps/tests/test_blending_steps.py

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,50 @@
1010

1111
# fmt:off
1212
steps_arg_values = [
13-
(1, 3, 4, 8, None, None, False, "spn", True, 4, False, False, 0, False, None),
14-
(1, 3, 4, 8, "obs", None, False, "spn", True, 4, False, False, 0, False, None),
15-
(1, 3, 4, 8, "incremental", None, False, "spn", True, 4, False, False, 0, False, None),
16-
(1, 3, 4, 8, None, "mean", False, "spn", True, 4, False, False, 0, False, None),
17-
(1, 3, 4, 8, None, "mean", False, "spn", True, 4, False, False, 0, True, None),
18-
(1, 3, 4, 8, None, "cdf", False, "spn", True, 4, False, False, 0, False, None),
19-
(1, [1, 2, 3], 4, 8, None, "cdf", False, "spn", True, 4, False, False, 0, False, None),
20-
(1, 3, 4, 8, "incremental", "cdf", False, "spn", True, 4, False, False, 0, False, None),
21-
(1, 3, 4, 6, "incremental", "cdf", False, "bps", True, 4, False, False, 0, False, None),
22-
(1, 3, 4, 6, "incremental", "cdf", False, "bps", False, 4, False, False, 0, False, None),
23-
(1, 3, 4, 6, "incremental", "cdf", False, "bps", False, 4, False, False, 0, True, None),
24-
(1, 3, 4, 9, "incremental", "cdf", False, "spn", True, 4, False, False, 0, False, None),
25-
(2, 3, 10, 8, "incremental", "cdf", False, "spn", True, 10, False, False, 0, False, None),
26-
(5, 3, 5, 8, "incremental", "cdf", False, "spn", True, 5, False, False, 0, False, None),
27-
(1, 10, 1, 8, "incremental", "cdf", False, "spn", True, 1, False, False, 0, False, None),
28-
(2, 3, 2, 8, "incremental", "cdf", True, "spn", True, 2, False, False, 0, False, None),
29-
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, False, 0, False, None),
30-
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, False, 0, False, "bps"),
31-
# TODO: make next test work! This is currently not working on the main branch
32-
# (2, 3, 4, 8, "incremental", "cdf", True, "spn", True, 2, False, False, 0, False),
33-
# (2, 3, 4, 8, "incremental", "cdf", False, "spn", True, 2, False, False, 0, False),
13+
(1, 3, 4, 8, None, None, False, "spn", True, 4, False, False, 0, False, None, None),
14+
(1, 3, 4, 8, "obs", None, False, "spn", True, 4, False, False, 0, False, None, None),
15+
(1, 3, 4, 8, "incremental", None, False, "spn", True, 4, False, False, 0, False, None, None),
16+
(1, 3, 4, 8, None, "mean", False, "spn", True, 4, False, False, 0, False, None, None),
17+
(1, 3, 4, 8, None, "mean", False, "spn", True, 4, False, False, 0, True, None, None),
18+
(1, 3, 4, 8, None, "cdf", False, "spn", True, 4, False, False, 0, False, None, None),
19+
(1, [1, 2, 3], 4, 8, None, "cdf", False, "spn", True, 4, False, False, 0, False, None, None),
20+
(1, 3, 4, 8, "incremental", "cdf", False, "spn", True, 4, False, False, 0, False, None, None),
21+
(1, 3, 4, 6, "incremental", "cdf", False, "bps", True, 4, False, False, 0, False, None, None),
22+
(1, 3, 4, 6, "incremental", "cdf", False, "bps", False, 4, False, False, 0, False, None, None),
23+
(1, 3, 4, 6, "incremental", "cdf", False, "bps", False, 4, False, False, 0, True, None, None),
24+
(1, 3, 4, 9, "incremental", "cdf", False, "spn", True, 4, False, False, 0, False, None, None),
25+
(2, 3, 10, 8, "incremental", "cdf", False, "spn", True, 10, False, False, 0, False, None, None),
26+
(5, 3, 5, 8, "incremental", "cdf", False, "spn", True, 5, False, False, 0, False, None, None),
27+
(1, 10, 1, 8, "incremental", "cdf", False, "spn", True, 1, False, False, 0, False, None, None),
28+
(2, 3, 2, 8, "incremental", "cdf", True, "spn", True, 2, False, False, 0, False, None, None),
29+
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, False, 0, False, None, None),
30+
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, False, 0, False, "bps", None),
3431
# Test the case where the radar image contains no rain.
35-
(1, 3, 6, 8, None, None, False, "spn", True, 6, True, False, 0, False, None),
36-
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, False, 0, False, None),
37-
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, False, 0, True, None),
32+
(1, 3, 6, 8, None, None, False, "spn", True, 6, True, False, 0, False, None, None),
33+
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, False, 0, False, None, None),
34+
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, False, 0, True, None, None),
3835
# Test the case where the NWP fields contain no rain.
39-
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, True, 0, False, None),
40-
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, False, True, 0, True, None),
36+
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, True, 0, False, None, None),
37+
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, False, True, 0, True, None, None),
4138
# Test the case where both the radar image and the NWP fields contain no rain.
42-
(1, 3, 6, 8, None, None, False, "spn", True, 6, True, True, 0, False, None),
43-
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, True, 0, False, None),
44-
(5, 3, 5, 6, "obs", "mean", True, "spn", True, 5, True, True, 0, False, None),
39+
(1, 3, 6, 8, None, None, False, "spn", True, 6, True, True, 0, False, None, None),
40+
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, True, 0, False, None, None),
41+
(5, 3, 5, 6, "obs", "mean", True, "spn", True, 5, True, True, 0, False, None, None),
4542
# Test for smooth radar mask
46-
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, False, 80, False, None),
47-
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, False, False, 80, False, None),
48-
(5, 3, 5, 6, "obs", "mean", False, "spn", False, 5, False, False, 80, False, None),
49-
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, True, 80, False, None),
50-
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, False, 80, True, None),
51-
(5, 3, 5, 6, "obs", "mean", False, "spn", False, 5, True, True, 80, False, None),
52-
(5, [1, 2, 3], 5, 6, "obs", "mean", False, "spn", False, 5, True, True, 80, False, None),
53-
(5, [1, 3], 5, 6, "obs", "mean", False, "spn", False, 5, True, True, 80, False, None),
43+
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, False, 80, False, None, None),
44+
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, False, False, 80, False, None, None),
45+
(5, 3, 5, 6, "obs", "mean", False, "spn", False, 5, False, False, 80, False, None, None),
46+
(1, 3, 6, 8, None, None, False, "spn", True, 6, False, True, 80, False, None, None),
47+
(5, 3, 5, 6, "incremental", "cdf", False, "spn", False, 5, True, False, 80, True, None, None),
48+
(5, 3, 5, 6, "obs", "mean", False, "spn", False, 5, True, True, 80, False, None, None),
49+
(5, [1, 2, 3], 5, 6, "obs", "mean", False, "spn", False, 5, True, True, 80, False, None, None),
50+
(5, [1, 3], 5, 6, "obs", "mean", False, "spn", False, 5, True, True, 80, False, None, None),
51+
# Test the usage of a max_mask_rim in the mask_kwargs
52+
(1, 3, 6, 8, None, None, False, "bps", True, 6, False, False, 80, False, None, 40),
53+
(5, 3, 5, 6, "obs", "mean", False, "bps", False, 5, False, False, 80, False, None, 40),
54+
(5, 3, 5, 6, "incremental", "cdf", False, "bps", False, 5, False, False, 80, False, None, 25),
55+
(5, 3, 5, 6, "incremental", "cdf", False, "bps", False, 5, False, False, 80, False, None, 40),
56+
(5, 3, 5, 6, "incremental", "cdf", False, "bps", False, 5, False, False, 80, False, None, 60),
5457
]
5558
# fmt:on
5659

@@ -70,6 +73,7 @@
7073
"smooth_radar_mask_range",
7174
"resample_distribution",
7275
"vel_pert_method",
76+
"max_mask_rim",
7377
)
7478

7579

@@ -90,6 +94,7 @@ def test_steps_blending(
9094
smooth_radar_mask_range,
9195
resample_distribution,
9296
vel_pert_method,
97+
max_mask_rim,
9398
):
9499
pytest.importorskip("cv2")
95100

@@ -162,13 +167,18 @@ def test_steps_blending(
162167
metadata["zr_a"] = 200.0
163168
metadata["zr_b"] = 1.6
164169

165-
# Also set the outdir_path and clim_kwargs
170+
# Also set the outdir_path, clim_kwargs and mask_kwargs
166171
outdir_path_skill = "./tmp/"
167172
if n_models == 1:
168173
clim_kwargs = None
169174
else:
170175
clim_kwargs = dict({"n_models": n_models, "window_length": 30})
171176

177+
if max_mask_rim is not None:
178+
mask_kwargs = dict({"mask_rim": 10, "max_mask_rim": max_mask_rim})
179+
else:
180+
mask_kwargs = None
181+
172182
###
173183
# First threshold the data and convert it to dBR
174184
###
@@ -288,6 +298,7 @@ def test_steps_blending(
288298
conditional=False,
289299
probmatching_method=probmatching_method,
290300
mask_method=mask_method,
301+
resample_distribution=resample_distribution,
291302
smooth_radar_mask_range=smooth_radar_mask_range,
292303
callback=None,
293304
return_output=True,
@@ -301,7 +312,7 @@ def test_steps_blending(
301312
noise_kwargs=None,
302313
vel_pert_kwargs=None,
303314
clim_kwargs=clim_kwargs,
304-
mask_kwargs=None,
315+
mask_kwargs=mask_kwargs,
305316
measure_time=False,
306317
)
307318

0 commit comments

Comments
 (0)