Skip to content

Commit 4c43aa4

Browse files
mats-knmidnerini
andauthoredMar 3, 2025··
fix noise tapering and error handling (#460)
* fix noise tapering and error handling * slight reduction in skill * add generic check_norain method * Add test for all-zero steps nowcast (this will fail) * extend error message * Add test for no rain to all nowcast methods * Fix tests * More fixes for tests * fix most nowcasts for norain * fix linda nowcast * update error message and docstring * fix black * set default check norain win fun to none * re insert accidentally removed notes from sseps * Update pysteps/blending/utils.py Co-authored-by: Daniele Nerini <daniele.nerini@gmail.com> --------- Co-authored-by: Daniele Nerini <daniele.nerini@gmail.com>
1 parent 9dc68c5 commit 4c43aa4

16 files changed

+483
-87
lines changed
 

‎pysteps/blending/steps.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from pysteps.nowcasts import utils as nowcast_utils
5757
from pysteps.postprocessing import probmatching
5858
from pysteps.timeseries import autoregression, correlation
59+
from pysteps.utils.check_norain import check_norain
5960

6061
try:
6162
import dask
@@ -77,7 +78,7 @@ class StepsBlendingConfig:
7778
precip_threshold: float, optional
7879
Specifies the threshold value for minimum observable precipitation
7980
intensity. Required if mask_method is not None or conditional is True.
80-
norain_threshold: float, optional
81+
norain_threshold: float
8182
Specifies the threshold value for the fraction of rainy (see above) pixels
8283
in the radar rainfall field below which we consider there to be no rain.
8384
Depends on the amount of clutter typically present.
@@ -435,7 +436,6 @@ def __init__(
435436

436437
# Additional variables for time measurement
437438
self.__start_time_init = None
438-
self.__zero_precip_time = None
439439
self.__init_time = None
440440
self.__mainloop_time = None
441441

@@ -777,7 +777,7 @@ def __check_inputs(self):
777777
self.__params.filter_kwargs = deepcopy(self.__config.filter_kwargs)
778778

779779
if self.__config.noise_kwargs is None:
780-
self.__params.noise_kwargs = dict()
780+
self.__params.noise_kwargs = {"win_fun": "tukey"}
781781
else:
782782
self.__params.noise_kwargs = deepcopy(self.__config.noise_kwargs)
783783

@@ -1093,17 +1093,19 @@ def transform_to_lagrangian(precip, i):
10931093
self.__precip_models = np.stack(temp_precip_models)
10941094

10951095
# Check for zero input fields in the radar and NWP data.
1096-
self.__params.zero_precip_radar = blending.utils.check_norain(
1096+
self.__params.zero_precip_radar = check_norain(
10971097
self.__precip,
10981098
self.__config.precip_threshold,
10991099
self.__config.norain_threshold,
1100+
self.__params.noise_kwargs["win_fun"],
11001101
)
11011102
# The norain fraction threshold used for nwp is the default value of 0.0,
11021103
# since nwp does not suffer from clutter.
1103-
self.__params.zero_precip_model_fields = blending.utils.check_norain(
1104+
self.__params.zero_precip_model_fields = check_norain(
11041105
self.__precip_models,
11051106
self.__config.precip_threshold,
11061107
self.__config.norain_threshold,
1108+
self.__params.noise_kwargs["win_fun"],
11071109
)
11081110

11091111
def __zero_precipitation_forecast(self):
@@ -1141,7 +1143,7 @@ def __zero_precipitation_forecast(self):
11411143
precip_forecast_workers = None
11421144

11431145
if self.__config.measure_time:
1144-
self.__zero_precip_time = time.time() - self.__start_time_init
1146+
zero_precip_time = time.time() - self.__start_time_init
11451147

11461148
if self.__config.return_output:
11471149
precip_forecast_all_members_all_times = np.stack(
@@ -1154,8 +1156,8 @@ def __zero_precipitation_forecast(self):
11541156
if self.__config.measure_time:
11551157
return (
11561158
precip_forecast_all_members_all_times,
1157-
self.__zero_precip_time,
1158-
self.__zero_precip_time,
1159+
zero_precip_time,
1160+
zero_precip_time,
11591161
)
11601162
else:
11611163
return precip_forecast_all_members_all_times
@@ -1177,10 +1179,11 @@ def __prepare_nowcast_for_zero_radar(self):
11771179
if done:
11781180
break
11791181
for j in range(self.__precip_models.shape[0]):
1180-
if not blending.utils.check_norain(
1182+
if not check_norain(
11811183
self.__precip_models[j, t],
11821184
self.__config.precip_threshold,
11831185
self.__config.norain_threshold,
1186+
self.__params.noise_kwargs["win_fun"],
11841187
):
11851188
if self.__state.precip_models_cascades is not None:
11861189
self.__state.precip_cascades[
@@ -2925,7 +2928,7 @@ def forecast(
29252928
precip_thr: float, optional
29262929
Specifies the threshold value for minimum observable precipitation
29272930
intensity. Required if mask_method is not None or conditional is True.
2928-
norain_thr: float, optional
2931+
norain_thr: float
29292932
Specifies the threshold value for the fraction of rainy (see above) pixels
29302933
in the radar rainfall field below which we consider there to be no rain.
29312934
Depends on the amount of clutter typically present.

‎pysteps/blending/utils.py

+6-10
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
decompose_NWP
1616
compute_store_nwp_motion
1717
load_NWP
18-
check_norain
1918
compute_smooth_dilated_mask
2019
"""
2120

2221
import datetime
22+
import warnings
2323
from pathlib import Path
2424

2525
import numpy as np
@@ -28,6 +28,7 @@
2828
from pysteps.cascade.bandpass_filters import filter_gaussian
2929
from pysteps.exceptions import MissingOptionalDependency
3030
from pysteps.utils import get_method as utils_get_method
31+
from pysteps.utils.check_norain import check_norain as new_check_norain
3132

3233
try:
3334
import netCDF4
@@ -534,7 +535,7 @@ def load_NWP(input_nc_path_decomp, input_path_velocities, start_time, n_timestep
534535

535536
def check_norain(precip_arr, precip_thr=None, norain_thr=0.0):
536537
"""
537-
538+
DEPRECATED use :py:mod:`pysteps.utils.check_norain.check_norain` in stead
538539
Parameters
539540
----------
540541
precip_arr: array-like
@@ -551,15 +552,10 @@ def check_norain(precip_arr, precip_thr=None, norain_thr=0.0):
551552
Returns whether the fraction of rainy pixels is below the norain_thr threshold.
552553
553554
"""
554-
555-
if precip_thr is None:
556-
precip_thr = np.nanmin(precip_arr)
557-
rain_pixels = precip_arr[precip_arr > precip_thr]
558-
norain = rain_pixels.size / precip_arr.size <= norain_thr
559-
print(
560-
f"Rain fraction is: {str(rain_pixels.size / precip_arr.size)}, while minimum fraction is {str(norain_thr)}"
555+
warnings.warn(
556+
"pysteps.blending.utils.check_norain has been deprecated, use pysteps.utils.check_norain.check_norain instead"
561557
)
562-
return norain
558+
return new_check_norain(precip_arr, precip_thr, norain_thr, None)
563559

564560

565561
def compute_smooth_dilated_mask(

‎pysteps/noise/fftgenerators.py

+29-4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
import numpy as np
4848
from scipy import optimize
49+
4950
from .. import utils
5051

5152

@@ -99,7 +100,13 @@ def initialize_param_2d_fft_filter(field, **kwargs):
99100
if len(field.shape) < 2 or len(field.shape) > 3:
100101
raise ValueError("the input is not two- or three-dimensional array")
101102
if np.any(~np.isfinite(field)):
102-
raise ValueError("field contains non-finite values")
103+
raise ValueError(
104+
"field contains non-finite values, this typically happens when the input\n"
105+
+ "precipitation field provided to pysteps contains (mostly)zero values.\n"
106+
+ "To prevent this error please call pysteps.utils.check_norain first,\n"
107+
+ "using the same win_fun as used in this method (tukey by default)\n"
108+
+ "and then only call this method if that check fails."
109+
)
103110

104111
# defaults
105112
win_fun = kwargs.get("win_fun", None)
@@ -254,7 +261,13 @@ def initialize_nonparam_2d_fft_filter(field, **kwargs):
254261
if len(field.shape) < 2 or len(field.shape) > 3:
255262
raise ValueError("the input is not two- or three-dimensional array")
256263
if np.any(~np.isfinite(field)):
257-
raise ValueError("field contains non-finite values")
264+
raise ValueError(
265+
"field contains non-finite values, this typically happens when the input\n"
266+
+ "precipitation field provided to pysteps contains (mostly)zero values.\n"
267+
+ "To prevent this error please call pysteps.utils.check_norain first,\n"
268+
+ "using the same win_fun as used in this method (tukey by default)\n"
269+
+ "and then only call this method if that check fails."
270+
)
258271

259272
# defaults
260273
win_fun = kwargs.get("win_fun", "tukey")
@@ -361,7 +374,13 @@ def generate_noise_2d_fft_filter(
361374
if len(F.shape) != 2:
362375
raise ValueError("field is not two-dimensional array")
363376
if np.any(~np.isfinite(F)):
364-
raise ValueError("field contains non-finite values")
377+
raise ValueError(
378+
"field contains non-finite values, this typically happens when the input\n"
379+
+ "precipitation field provided to pysteps contains (mostly)zero values.\n"
380+
+ "To prevent this error please call pysteps.utils.check_norain first,\n"
381+
+ "using the same win_fun as used in this method (tukey by default)\n"
382+
+ "and then only call this method if that check fails."
383+
)
365384

366385
if randstate is None:
367386
randstate = np.random
@@ -755,7 +774,13 @@ def generate_noise_2d_ssft_filter(F, randstate=None, seed=None, **kwargs):
755774
if len(F.shape) != 4:
756775
raise ValueError("the input is not four-dimensional array")
757776
if np.any(~np.isfinite(F)):
758-
raise ValueError("field contains non-finite values")
777+
raise ValueError(
778+
"field contains non-finite values, this typically happens when the input\n"
779+
+ "precipitation field provided to pysteps contains (mostly) zero value.s\n"
780+
+ "To prevent this error please call pysteps.utils.check_norain first,\n"
781+
+ "using the same win_fun as used in this method (tukey by default)\n"
782+
+ "and then only call this method if that check fails."
783+
)
759784

760785
if "domain" in kwargs.keys() and kwargs["domain"] == "spectral":
761786
raise NotImplementedError(

‎pysteps/nowcasts/linda.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,23 @@
4040
import time
4141
import warnings
4242

43+
from pysteps.utils.check_norain import check_norain
44+
4345
try:
4446
import dask
4547

4648
DASK_IMPORTED = True
4749
except ImportError:
4850
DASK_IMPORTED = False
4951
import numpy as np
52+
from scipy import optimize as opt
53+
from scipy import stats
5054
from scipy.integrate import nquad
5155
from scipy.interpolate import interp1d
52-
from scipy import optimize as opt
5356
from scipy.signal import convolve
54-
from scipy import stats
5557

5658
from pysteps import extrapolation, feature, noise
57-
from pysteps.nowcasts.utils import nowcast_main_loop
59+
from pysteps.nowcasts.utils import nowcast_main_loop, zero_precipitation_forecast
5860

5961

6062
def forecast(
@@ -292,6 +294,19 @@ def forecast(
292294
True if np.any(~np.isfinite(precip)) else False
293295
)
294296

297+
starttime_init = time.time()
298+
299+
if check_norain(precip, 0.0, 0.0, None):
300+
return zero_precipitation_forecast(
301+
n_ens_members if nowcast_type == "ensemble" else None,
302+
timesteps,
303+
precip,
304+
callback,
305+
return_output,
306+
measure_time,
307+
starttime_init,
308+
)
309+
295310
forecast_gen = _linda_deterministic_init(
296311
precip,
297312
velocity,

‎pysteps/nowcasts/sprog.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
forecast
1111
"""
1212

13-
import numpy as np
1413
import time
1514

16-
from pysteps import cascade
17-
from pysteps import extrapolation
18-
from pysteps import utils
15+
import numpy as np
16+
17+
from pysteps import cascade, extrapolation, utils
1918
from pysteps.nowcasts import utils as nowcast_utils
19+
from pysteps.nowcasts.utils import compute_percentile_mask, nowcast_main_loop
2020
from pysteps.postprocessing import probmatching
2121
from pysteps.timeseries import autoregression, correlation
22-
from pysteps.nowcasts.utils import compute_percentile_mask, nowcast_main_loop
22+
from pysteps.utils.check_norain import check_norain
2323

2424
try:
2525
import dask
@@ -34,6 +34,7 @@ def forecast(
3434
velocity,
3535
timesteps,
3636
precip_thr=None,
37+
norain_thr=0.0,
3738
n_cascade_levels=6,
3839
extrap_method="semilagrangian",
3940
decomp_method="fft",
@@ -68,6 +69,11 @@ def forecast(
6869
of the list are required to be in ascending order.
6970
precip_thr: float, required
7071
The threshold value for minimum observable precipitation intensity.
72+
norain_thr: float
73+
Specifies the threshold value for the fraction of rainy (see above) pixels
74+
in the radar rainfall field below which we consider there to be no rain.
75+
Depends on the amount of clutter typically present.
76+
Standard set to 0.0
7177
n_cascade_levels: int, optional
7278
The number of cascade levels to use. Defaults to 6, see issue #385
7379
on GitHub.
@@ -182,6 +188,8 @@ def forecast(
182188

183189
if measure_time:
184190
starttime_init = time.time()
191+
else:
192+
starttime_init = None
185193

186194
fft = utils.get_method(fft_method, shape=precip.shape[1:], n_threads=num_workers)
187195

@@ -203,6 +211,11 @@ def forecast(
203211
[~np.isfinite(precip[i, :]) for i in range(precip.shape[0])]
204212
)
205213

214+
if check_norain(precip, precip_thr, norain_thr, None):
215+
return nowcast_utils.zero_precipitation_forecast(
216+
None, timesteps, precip, None, True, measure_time, starttime_init
217+
)
218+
206219
# determine the precipitation threshold mask
207220
if conditional:
208221
mask_thr = np.logical_and.reduce(

‎pysteps/nowcasts/sseps.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,16 @@
1818
forecast
1919
"""
2020

21-
import numpy as np
2221
import time
23-
from scipy.ndimage import generate_binary_structure, iterate_structure
2422

23+
import numpy as np
24+
from scipy.ndimage import generate_binary_structure, iterate_structure
2525

26-
from pysteps import cascade
27-
from pysteps import extrapolation
28-
from pysteps import noise
26+
from pysteps import cascade, extrapolation, noise
2927
from pysteps.nowcasts import utils as nowcast_utils
3028
from pysteps.postprocessing import probmatching
3129
from pysteps.timeseries import autoregression, correlation
30+
from pysteps.utils.check_norain import check_norain
3231

3332
try:
3433
import dask
@@ -211,7 +210,7 @@ def forecast(
211210
filter_kwargs = dict()
212211

213212
if noise_kwargs is None:
214-
noise_kwargs = dict()
213+
noise_kwargs = {"win_fun": "tukey"}
215214

216215
if vel_pert_kwargs is None:
217216
vel_pert_kwargs = dict()
@@ -297,6 +296,8 @@ def forecast(
297296

298297
if measure_time:
299298
starttime_init = time.time()
299+
else:
300+
starttime_init = None
300301

301302
# get methods
302303
extrapolator_method = extrapolation.get_method(extrap_method)
@@ -312,6 +313,22 @@ def forecast(
312313
if noise_method is not None:
313314
init_noise, generate_noise = noise.get_method(noise_method)
314315

316+
if check_norain(
317+
precip,
318+
precip_thr,
319+
war_thr,
320+
noise_kwargs["win_fun"],
321+
):
322+
return nowcast_utils.zero_precipitation_forecast(
323+
n_ens_members,
324+
timesteps,
325+
precip,
326+
callback,
327+
return_output,
328+
measure_time,
329+
starttime_init,
330+
)
331+
315332
# advect the previous precipitation fields to the same position with the
316333
# most recent one (i.e. transform them into the Lagrangian coordinates)
317334
precip = precip[-(ar_order + 1) :, :, :].copy()

0 commit comments

Comments
 (0)
Please sign in to comment.