Repository: pySTEPS/pysteps (language: python)

Issue:
No reproducibility of steps blended nowcast when using noise_stddev_adj='auto'
When running 2 identical blended nowcasts with `pysteps.blending.steps.forecast` using  
a fixed  `seed`  
and setting  
`noise_stddev_adj = 'auto'`,  
the results (sometimes) differ.

I suspect this behavior is coming from the 
`pysteps.noise.utils.compute_noise_stddev_adjs` function.

This function takes the `seed` argument, but inside the function the `seed` is always set to `None`:
https://github.com/pySTEPS/pysteps/blob/be8eea432239f77bdfb7edf51a9bfaa94d012752/pysteps/noise/utils.py#L102-L106

Leading to different normalization weights for every run and impacting the final results, preventing reproducibility.

Kind regards,
@mpvginde 


Discussion / hints:
Hi @mpvginde thanks for reporting this issue! Do you want to go ahead and submit a pr (looks like a simple fix)? Otherwise I'll try to have a look on it 
Sure @dnerini. I'll have a look at it.
A small update:
The issue might be more complex as I initially thought.
When I fix passing the seed in the `pysteps.noise.utils.compute_noise_stddev_adjs` results become reproducable for the `pysteps.blending.steps.forecast` routine.
However, for `pysteps.nowcast.steps.forecast` results still differ between identical runs. 
Only when I disable the dask parallel workers in the `_update` helper routine in `nowcasts.steps` the results become reproducable. 
I suspect this might be linked to issue #337, I will investigate further. 

Fix patch:
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 5ed946c71..8e60e99b7 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -217,7 +217,7 @@ def set_root():
 latex_elements = {
     "papersize": "a4paper",
     "pointsize": "10pt",
-    "preamble": latex_preamble
+    "preamble": latex_preamble,
     # Latex figure (float) alignment
     #
     # 'figure_align': 'htbp',
diff --git a/pysteps/blending/steps.py b/pysteps/blending/steps.py
index 30d87349a..ca51c1017 100644
--- a/pysteps/blending/steps.py
+++ b/pysteps/blending/steps.py
@@ -514,6 +514,7 @@ def forecast(
     )
 
     # 2. Initialize the noise method
+    np.random.seed(seed)
     pp, generate_noise, noise_std_coeffs = _init_noise(
         precip,
         precip_thr,
@@ -526,6 +527,7 @@ def forecast(
         noise_stddev_adj,
         measure_time,
         num_workers,
+        seed,
     )
 
     # 3. Perform the cascade decomposition for the input precip fields and
@@ -1662,6 +1664,7 @@ def _init_noise(
     noise_stddev_adj,
     measure_time,
     num_workers,
+    seed,
 ):
     """Initialize the noise method."""
     if noise_method is None:
@@ -1690,6 +1693,7 @@ def _init_noise(
             20,
             conditional=True,
             num_workers=num_workers,
+            seed=seed,
         )
 
         if measure_time:
@@ -1944,7 +1948,6 @@ def _init_random_generators(
     if noise_method is not None:
         randgen_prec = []
         randgen_motion = []
-        np.random.seed(seed)
         for j in range(n_ens_members):
             rs = np.random.RandomState(seed)
             randgen_prec.append(rs)
diff --git a/pysteps/decorators.py b/pysteps/decorators.py
index 6f06af874..44fbaebdb 100644
--- a/pysteps/decorators.py
+++ b/pysteps/decorators.py
@@ -228,9 +228,9 @@ def _interpolator_with_preamble(xy_coord, values, xgrid, ygrid, **kwargs):
                 indy = 0
                 for subygrid in subygrids:
                     deltay = subygrid.size
-                    interpolated[
-                        :, indy : (indy + deltay), indx : (indx + deltax)
-                    ] = interpolator(xy_coord, values, subxgrid, subygrid, **kwargs)
+                    interpolated[:, indy : (indy + deltay), indx : (indx + deltax)] = (
+                        interpolator(xy_coord, values, subxgrid, subygrid, **kwargs)
+                    )
                     indy += deltay
                 indx += deltax
 
diff --git a/pysteps/io/importers.py b/pysteps/io/importers.py
index 44b4e1aaa..d7a9a850e 100644
--- a/pysteps/io/importers.py
+++ b/pysteps/io/importers.py
@@ -1366,9 +1366,9 @@ def import_odim_hdf5(filename, qty="RATE", **kwargs):
                                     mask = np.logical_and(~mask_u, ~mask_n)
                                     quality = np.empty(arr.shape)  # , dtype=float)
                                     quality[mask] = arr[mask] * gain + offset
-                                    quality[
-                                        ~mask
-                                    ] = np.nan  # a qui -----------------------------
+                                    quality[~mask] = (
+                                        np.nan
+                                    )  # a qui -----------------------------
 
     if precip is None:
         raise IOError("requested quantity %s not found" % qty)
diff --git a/pysteps/noise/fftgenerators.py b/pysteps/noise/fftgenerators.py
index cdf31417c..7ffabdc26 100644
--- a/pysteps/noise/fftgenerators.py
+++ b/pysteps/noise/fftgenerators.py
@@ -708,9 +708,7 @@ def initialize_nonparam_2d_nested_filter(field, gridres=1.0, **kwargs):
         # update indices
         level += 1
         Idxi, Idxj = _split_field((0, dim[0]), (0, dim[1]), 2**level)
-        Idxipsd, Idxjpsd = _split_field(
-            (0, 2**max_level), (0, 2**max_level), 2**level
-        )
+        Idxipsd, Idxjpsd = _split_field((0, 2**max_level), (0, 2**max_level), 2**level)
 
     return {"field": F, "input_shape": field.shape[1:], "use_full_fft": True}
 
diff --git a/pysteps/noise/utils.py b/pysteps/noise/utils.py
index 51aad3fd4..58495b8ad 100644
--- a/pysteps/noise/utils.py
+++ b/pysteps/noise/utils.py
@@ -96,39 +96,37 @@ def compute_noise_stddev_adjs(
 
     if dask_imported and num_workers > 1:
         res = []
-    else:
-        N_stds = []
 
+    N_stds = [None] * num_iter
     randstates = []
-    seed = None
+
     for k in range(num_iter):
         randstates.append(np.random.RandomState(seed=seed))
         seed = np.random.randint(0, high=1e9)
 
-    for k in range(num_iter):
-
-        def worker():
-            # generate Gaussian white noise field, filter it using the chosen
-            # method, multiply it with the standard deviation of the observed
-            # field and apply the precipitation mask
-            N = noise_generator(noise_filter, randstate=randstates[k], seed=seed)
-            N = N / np.std(N) * sigma + mu
-            N[~MASK] = R_thr_2
+    def worker(k):
+        # generate Gaussian white noise field, filter it using the chosen
+        # method, multiply it with the standard deviation of the observed
+        # field and apply the precipitation mask
+        N = noise_generator(noise_filter, randstate=randstates[k])
+        N = N / np.std(N) * sigma + mu
+        N[~MASK] = R_thr_2
 
-            # subtract the mean and decompose the masked noise field into a
-            # cascade
-            N -= mu
-            decomp_N = decomp_method(N, F, mask=MASK_)
+        # subtract the mean and decompose the masked noise field into a
+        # cascade
+        N -= mu
+        decomp_N = decomp_method(N, F, mask=MASK_)
 
-            return decomp_N["stds"]
-
-        if dask_imported and num_workers > 1:
-            res.append(dask.delayed(worker)())
-        else:
-            N_stds.append(worker())
+        N_stds[k] = decomp_N["stds"]
 
     if dask_imported and num_workers > 1:
-        N_stds = dask.compute(*res, num_workers=num_workers)
+        for k in range(num_iter):
+            res.append(dask.delayed(worker)(k))
+        dask.compute(*res, num_workers=num_workers)
+
+    else:
+        for k in range(num_iter):
+            worker(k)
 
     # for each cascade level, compare the standard deviations between the
     # observed field and the masked noise field, which gives the correction
diff --git a/pysteps/nowcasts/linda.py b/pysteps/nowcasts/linda.py
index 5337385c3..7d737eaa9 100644
--- a/pysteps/nowcasts/linda.py
+++ b/pysteps/nowcasts/linda.py
@@ -572,8 +572,7 @@ def _compute_window_weights(coords, grid_height, grid_width, window_radius):
             dx = c[1] - grid_x
 
             w[i, :] = np.exp(
-                -dy * dy / (2 * window_radius_1**2)
-                - dx * dx / (2 * window_radius_2**2)
+                -dy * dy / (2 * window_radius_1**2) - dx * dx / (2 * window_radius_2**2)
             )
     else:
         w[0, :] = np.ones((grid_height, grid_width))
diff --git a/pysteps/nowcasts/sseps.py b/pysteps/nowcasts/sseps.py
index e834916da..4646ba3da 100644
--- a/pysteps/nowcasts/sseps.py
+++ b/pysteps/nowcasts/sseps.py
@@ -729,12 +729,12 @@ def worker(j):
                                 else:
                                     EPS_ = None
                                 # apply AR(p) process to cascade level
-                                precip_cascades[
-                                    i, :, :, :
-                                ] = autoregression.iterate_ar_model(
-                                    precip_cascades[i, :, :, :],
-                                    phi[m, n, i, :],
-                                    eps=EPS_,
+                                precip_cascades[i, :, :, :] = (
+                                    autoregression.iterate_ar_model(
+                                        precip_cascades[i, :, :, :],
+                                        phi[m, n, i, :],
+                                        eps=EPS_,
+                                    )
                                 )
                                 EPS_ = None
                             rc[m][n][j] = precip_cascades.copy()
diff --git a/pysteps/nowcasts/steps.py b/pysteps/nowcasts/steps.py
index b419fb1e3..1fd3f6d85 100644
--- a/pysteps/nowcasts/steps.py
+++ b/pysteps/nowcasts/steps.py
@@ -443,6 +443,7 @@ def f(precip, i):
         precip[i, ~np.isfinite(precip[i, :])] = np.nanmin(precip[i, :])
 
     if noise_method is not None:
+        np.random.seed(seed)
         # get methods for perturbations
         init_noise, generate_noise = noise.get_method(noise_method)
 
@@ -466,6 +467,7 @@ def f(precip, i):
                 20,
                 conditional=True,
                 num_workers=num_workers,
+                seed=seed,
             )
 
             if measure_time:
@@ -543,7 +545,6 @@ def f(precip, i):
     if noise_method is not None:
         randgen_prec = []
         randgen_motion = []
-        np.random.seed(seed)
         for _ in range(n_ens_members):
             rs = np.random.RandomState(seed)
             randgen_prec.append(rs)
@@ -706,7 +707,7 @@ def _check_inputs(precip, velocity, timesteps, ar_order):
 
 
 def _update(state, params):
-    precip_forecast_out = []
+    precip_forecast_out = [None] * params["n_ens_members"]
 
     if params["noise_method"] is None or params["mask_method"] == "sprog":
         for i in range(params["n_cascade_levels"]):
@@ -828,7 +829,7 @@ def worker(j):
 
         precip_forecast[params["domain_mask"]] = np.nan
 
-        precip_forecast_out.append(precip_forecast)
+        precip_forecast_out[j] = precip_forecast
 
     if (
         DASK_IMPORTED
diff --git a/pysteps/scripts/fit_vel_pert_params.py b/pysteps/scripts/fit_vel_pert_params.py
index cfc685f31..17dcb3684 100644
--- a/pysteps/scripts/fit_vel_pert_params.py
+++ b/pysteps/scripts/fit_vel_pert_params.py
@@ -51,9 +51,7 @@
     mu = dp_perp_sum / dp_perp_n
 
     std_perp.append(
-        np.sqrt(
-            (dp_perp_sq_sum - 2 * mu * dp_perp_sum + dp_perp_n * mu**2) / dp_perp_n
-        )
+        np.sqrt((dp_perp_sq_sum - 2 * mu * dp_perp_sum + dp_perp_n * mu**2) / dp_perp_n)
     )
 
 try:
diff --git a/pysteps/utils/interface.py b/pysteps/utils/interface.py
index 3bb7a44fc..ded0c3094 100644
--- a/pysteps/utils/interface.py
+++ b/pysteps/utils/interface.py
@@ -205,9 +205,9 @@ def donothing(R, metadata=None, *args, **kwargs):
     methods_objects["rm_rdisc"] = spectral.remove_rain_norain_discontinuity
 
     # tapering methods
-    methods_objects[
-        "compute_mask_window_function"
-    ] = tapering.compute_mask_window_function
+    methods_objects["compute_mask_window_function"] = (
+        tapering.compute_mask_window_function
+    )
     methods_objects["compute_window_function"] = tapering.compute_window_function
 
     # transformation methods
diff --git a/pysteps/visualization/utils.py b/pysteps/visualization/utils.py
index 1a871ca3e..55a0650b1 100644
--- a/pysteps/visualization/utils.py
+++ b/pysteps/visualization/utils.py
@@ -13,6 +13,7 @@
     get_geogrid
     get_basemap_axis
 """
+
 import warnings
 
 import matplotlib.pylab as plt


Test patch:
diff --git a/pysteps/tests/helpers.py b/pysteps/tests/helpers.py
index b31d940d0..85bd861f5 100644
--- a/pysteps/tests/helpers.py
+++ b/pysteps/tests/helpers.py
@@ -4,6 +4,7 @@
 
 Collection of helper functions for the testing suite.
 """
+
 from datetime import datetime
 
 import numpy as np
diff --git a/pysteps/tests/test_motion.py b/pysteps/tests/test_motion.py
index 808bbd8aa..8d198960a 100644
--- a/pysteps/tests/test_motion.py
+++ b/pysteps/tests/test_motion.py
@@ -349,7 +349,7 @@ def test_vet_padding():
             verbose=False,
             sectors=((16, 4, 2), (16, 4, 2)),
             options=dict(maxiter=5),
-            padding=padding
+            padding=padding,
             # We use only a few iterations since
             # we don't care about convergence in this test
         )