From 2cff0dde4e9b3401a4ed815cf0c03237bdb26ad2 Mon Sep 17 00:00:00 2001 From: Lars Bonnefoy Date: Wed, 17 Jun 2026 13:56:19 +0200 Subject: [PATCH 1/4] Added Warning when default NWP skill is loaded. --- pysteps/blending/clim.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pysteps/blending/clim.py b/pysteps/blending/clim.py index 82656b276..41cc43712 100644 --- a/pysteps/blending/clim.py +++ b/pysteps/blending/clim.py @@ -193,8 +193,13 @@ def calc_clim_skill( past_skill = np.array(None) # check if there is enough data to compute the climatological skill if not past_skill.any(): + print("WARNING: Past skill file is empty, using default BPS2006 skill") return get_default_skill(n_cascade_levels, n_models) elif past_skill.shape[0] < window_length: + print( + f"WARNING: Past skill file has less days ({past_skill.shape[0]})" + f"than expected ({window_length}). Using default BPS2006 skill" + ) return get_default_skill(n_cascade_levels, n_models) # reduce window if necessary else: From a77d79fa638258dcee07d28c764351250654bfd9 Mon Sep 17 00:00:00 2001 From: Lars Bonnefoy Date: Fri, 19 Jun 2026 10:18:44 +0200 Subject: [PATCH 2/4] Computing weights per NWP member --- pysteps/blending/steps.py | 53 +++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/pysteps/blending/steps.py b/pysteps/blending/steps.py index fc1530f01..8c2d8cc4b 100644 --- a/pysteps/blending/steps.py +++ b/pysteps/blending/steps.py @@ -416,8 +416,6 @@ class StepsBlendingState: # Final outputs final_blended_forecast: np.ndarray | None = None final_blended_forecast_non_perturbed: np.ndarray | None = None - weights: np.ndarray | None = None - weights_model_only: np.ndarray | None = None # Timing and indexing time_prev_timestep: list[float] | None = None @@ -426,9 +424,23 @@ class StepsBlendingState: is_nowcast_time_step: bool | None = None subtimestep_index: int | None = None - # Weights used for blending - weights: np.ndarray | None = None + # Weights used for blending. `weights` / `weights_model_only` are per-worker + # scratch (the current member's weights, used by the downstream blends). + # `weights_per_member` / `weights_model_only_per_member` persist each member's + # weights across timesteps so members do not overwrite one another and the + # full-NWP smoothing reads each member's own previous-timestep weights. + + # Current working copy: full weights (extrapolation + NWP + noise) + weights: np.ndarray | None = None + + # Current working copy: Extrapolation + radar removed (for when there is no radar) weights_model_only: np.ndarray | None = None + + # List index by j that survives over timesteps (full weights) + weights_per_member: list[Any] | None = None + + # List index by j that survives over timesteps (Extrapolation + radar removed) + weights_model_only_per_member: list[Any] | None = None # This is stores here as well because this is changed during the forecast loop and thus no longer part of the config extrapolation_kwargs: dict[str, Any] = field(default_factory=dict) @@ -665,7 +677,7 @@ def __blended_nowcast_main_loop(self): def worker(j): worker_state = copy(self.__state) self.__determine_NWP_skill_for_next_timestep(t, j, worker_state) - self.__determine_weights_per_component(t, worker_state) + self.__determine_weights_per_component(t, j, worker_state) self.__regress_extrapolation_and_noise_cascades(j, worker_state, t) self.__perturb_blend_and_advect_extrapolation_and_noise_to_current_timestep( t, j, worker_state @@ -1629,6 +1641,14 @@ def __prepare_forecast_loop(self): self.__state.previous_displacement_prob_matching = np.stack( [None for j in range(self.__config.n_ens_members)] ) + # Per-member blending weights, persisted across timesteps so members do + # not overwrite one another (see StepsBlendingState). + self.__state.weights_per_member = [ + None for j in range(self.__config.n_ens_members) + ] + self.__state.weights_model_only_per_member = [ + None for j in range(self.__config.n_ens_members) + ] self.__state.final_blended_forecast = [ [] for j in range(self.__config.n_ens_members) ] @@ -2128,11 +2148,16 @@ def __determine_NWP_skill_for_next_timestep(self, t, j, worker_state): axis=0, ) - def __determine_weights_per_component(self, t, worker_state): + def __determine_weights_per_component(self, t, j, worker_state): """ Compute blending weights for each component based on the selected method ('bps' or 'spn'). Weights are determined for both full blending and model-only scenarios, accounting for correlations and covariance. + + The resulting weights are stored per ensemble member ``j`` in + ``weights_per_member`` / ``weights_model_only_per_member`` so members do + not overwrite one another and the full-NWP smoothing reads each member's + own previous-timestep weights. """ start_smoothing_to_final_weights = False if self.__config.timestep_start_full_nwp_weight is not None: @@ -2149,7 +2174,7 @@ def __determine_weights_per_component(self, t, worker_state): ) else: worker_state.weights = calculate_end_weights( - previous_weights=self.__state.weights, + previous_weights=worker_state.weights_per_member[j], timestep=t, n_timesteps=self.__timesteps[-1], start_full_nwp_weight=self.__config.timestep_start_full_nwp_weight, @@ -2212,7 +2237,7 @@ def __determine_weights_per_component(self, t, worker_state): ) elif start_smoothing_to_final_weights: worker_state.weights_model_only = calculate_end_weights( - previous_weights=self.__state.weights_model_only, + previous_weights=worker_state.weights_model_only_per_member[j], timestep=t, n_timesteps=self.__timesteps[-1], start_full_nwp_weight=self.__config.timestep_start_full_nwp_weight, @@ -2223,8 +2248,13 @@ def __determine_weights_per_component(self, t, worker_state): "Unknown weights method %s: must be 'bps' or 'spn'" % self.__config.weights_method ) - self.__state.weights = worker_state.weights - self.__state.weights_model_only = worker_state.weights_model_only + # Persist this member's weights without overwriting other members. The + # shallow copy shares the same list object, so index assignment touches a + # disjoint element per member (mirrors previous_displacement[j]). + worker_state.weights_per_member[j] = worker_state.weights + worker_state.weights_model_only_per_member[j] = ( + worker_state.weights_model_only + ) def __regress_extrapolation_and_noise_cascades(self, j, worker_state, t): """ @@ -2921,7 +2951,8 @@ def __blend_cascades(self, t_sub, j, worker_state): covariance=covariance_nwp_models, ) - self.__state.weights = worker_state.weights + # Persist this member's (spn) weights without overwriting other members. + worker_state.weights_per_member[j] = worker_state.weights # Create weights_with_noise to ensure there is always a 3D weights field, even # if self.__config.nowcasting_method is "external_nowcast" and n_ens_members is 1. From fe37a8c3aa78f1eaf192a14e98ed4deb88b7b91e Mon Sep 17 00:00:00 2001 From: Lars Bonnefoy Date: Fri, 19 Jun 2026 10:53:34 +0200 Subject: [PATCH 3/4] Black formatting --- pysteps/blending/steps.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pysteps/blending/steps.py b/pysteps/blending/steps.py index 8c2d8cc4b..9080eb4a2 100644 --- a/pysteps/blending/steps.py +++ b/pysteps/blending/steps.py @@ -431,11 +431,11 @@ class StepsBlendingState: # full-NWP smoothing reads each member's own previous-timestep weights. # Current working copy: full weights (extrapolation + NWP + noise) - weights: np.ndarray | None = None + weights: np.ndarray | None = None # Current working copy: Extrapolation + radar removed (for when there is no radar) weights_model_only: np.ndarray | None = None - + # List index by j that survives over timesteps (full weights) weights_per_member: list[Any] | None = None @@ -2252,9 +2252,7 @@ def __determine_weights_per_component(self, t, j, worker_state): # shallow copy shares the same list object, so index assignment touches a # disjoint element per member (mirrors previous_displacement[j]). worker_state.weights_per_member[j] = worker_state.weights - worker_state.weights_model_only_per_member[j] = ( - worker_state.weights_model_only - ) + worker_state.weights_model_only_per_member[j] = worker_state.weights_model_only def __regress_extrapolation_and_noise_cascades(self, j, worker_state, t): """ From 7fc63982d2311993d02d8fc408dc8e6cc5533e11 Mon Sep 17 00:00:00 2001 From: Lars Bonnefoy <113991871+larsbonnefoy@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:33:31 +0200 Subject: [PATCH 4/4] Update pysteps/blending/clim.py Co-authored-by: Ruben Imhoff <31476760+RubenImhoff@users.noreply.github.com> --- pysteps/blending/clim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysteps/blending/clim.py b/pysteps/blending/clim.py index 41cc43712..886fcb116 100644 --- a/pysteps/blending/clim.py +++ b/pysteps/blending/clim.py @@ -197,7 +197,7 @@ def calc_clim_skill( return get_default_skill(n_cascade_levels, n_models) elif past_skill.shape[0] < window_length: print( - f"WARNING: Past skill file has less days ({past_skill.shape[0]})" + f"WARNING: Past skill file has fewer days ({past_skill.shape[0]})" f"than expected ({window_length}). Using default BPS2006 skill" ) return get_default_skill(n_cascade_levels, n_models)