Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cbd0d06
Fix precip-noprecip input array blending failure
FelixE91 Mar 26, 2026
4d01c01
Add tests for blending with noprecip and precip input time steps
FelixE91 Mar 26, 2026
3948d5d
fix prev in check_previous_radar_obs()
FelixE91 Mar 26, 2026
5170768
Improve tests for blended forecast
ladc Mar 27, 2026
f3355c1
Fix formatting in blending/steps.py
ladc Mar 27, 2026
4650d8b
fix ar_order=1 runs (AR-1 model)
FelixE91 Mar 31, 2026
bd7f399
add warning message for ar_order 1
FelixE91 Apr 1, 2026
b49902e
Fix precip-noprecip input array nowcast failure
FelixE91 Apr 1, 2026
9135618
fix mixed precip blending test
FelixE91 Apr 1, 2026
0f45439
Add new nowcast test for mixed precip-no-precip input
FelixE91 Apr 1, 2026
3c723ce
improve warning messages in check_previous_radar_obs
FelixE91 Apr 1, 2026
7d3a2df
fix renormalize cascade
FelixE91 Apr 1, 2026
2cb883f
Fix n_ens_members to 1 in blending tests and update nowcast test para…
ladc Apr 1, 2026
26fa133
Remove more leftovers from blending from the test_nowcasts_steps.py s…
ladc Apr 1, 2026
e605150
reformatted to match default
FelixE91 Apr 2, 2026
c47d49d
black reformatting
FelixE91 Apr 3, 2026
3188d38
black reformatted
FelixE91 Apr 3, 2026
1e05808
fix codacy issue
FelixE91 Apr 3, 2026
2cc64a4
add bandit config to exclude assert check in tests
FelixE91 Apr 3, 2026
12d10a5
Improve code readability
FelixE91 Apr 3, 2026
55261f1
black and codacy checks
FelixE91 Apr 7, 2026
4e0b231
add warning message
FelixE91 Apr 7, 2026
2bcc065
fix codacy f-string
FelixE91 Apr 7, 2026
f8602b5
Fix naming
FelixE91 Apr 8, 2026
62f03cc
black check
FelixE91 Apr 8, 2026
ff2bc18
use check_norain in check_previous_radar_obs
FelixE91 Apr 9, 2026
15caf3a
fix codacy issue
FelixE91 Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ repos:
hooks:
- id: black
language_version: python3

- repo: https://github.com/PyCQA/bandit
rev: 1.9.4
hooks:
- id: bandit
args: ["-c", "bandit.yaml"]
additional_dependencies: ["bandit[yaml]"]
3 changes: 3 additions & 0 deletions bandit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# skip check for assert usage in the tests
assert_used:
skips: ['*test_*.py']
28 changes: 21 additions & 7 deletions pysteps/blending/skill_scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ def lt_dependent_cor_nwp(lt, correlations, outdir_path, n_model=0, skill_kwargs=
return rho


def lt_dependent_cor_extrapolation(PHI, correlations=None, correlations_prev=None):
def lt_dependent_cor_extrapolation(
PHI, correlations=None, correlations_prev=None, ar_order=2
):
"""Determine the correlation of the extrapolation (nowcast) component for
lead time lt and cascade k, by assuming that the correlation determined at
t=0 regresses towards the climatological values.
Expand All @@ -153,6 +155,8 @@ def lt_dependent_cor_extrapolation(PHI, correlations=None, correlations_prev=Non
be found from the AR-2 model.
correlations_prev : array-like, optional
Similar to correlations, but from the timestep before that.
ar_order : int, optional
The order of the autoregressive model to use. Must be >= 1.

Returns
-------
Expand All @@ -170,12 +174,22 @@ def lt_dependent_cor_extrapolation(PHI, correlations=None, correlations_prev=Non
if correlations_prev is None:
correlations_prev = np.repeat(1.0, PHI.shape[0])
# Same for correlations at first time step, we set it to
# phi1 / (1 - phi2), see BPS2004
if correlations is None:
correlations = PHI[:, 0] / (1.0 - PHI[:, 1])

# Calculate the correlation for lead time lt
rho = PHI[:, 0] * correlations + PHI[:, 1] * correlations_prev
if ar_order == 1:
# Yule-Walker equation for AR-1 model -> just PHI
if correlations is None:
correlations = PHI[:, 0]
# Calculate the correlation for lead time lt (AR-1)
rho = PHI[:, 0] * correlations
elif ar_order == 2:
# phi1 / (1 - phi2), see BPS2004
if correlations is None:
correlations = PHI[:, 0] / (1.0 - PHI[:, 1])
# Calculate the correlation for lead time lt (AR-2)
rho = PHI[:, 0] * correlations + PHI[:, 1] * correlations_prev
else:
raise ValueError(
"Autoregression of higher order than 2 is not defined. Please set ar_order = 2."
)

# Finally, set the current correlations array as the previous one for the
# next time step
Expand Down
64 changes: 53 additions & 11 deletions pysteps/blending/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
from pysteps.nowcasts import utils as nowcast_utils
from pysteps.postprocessing import probmatching
from pysteps.timeseries import autoregression, correlation
from pysteps.utils.check_norain import check_norain
from pysteps.utils.check_norain import check_norain, check_previous_radar_obs

try:
import dask
Expand Down Expand Up @@ -1196,12 +1196,22 @@ def transform_to_lagrangian(precip, i):
self.__precip_models = np.stack(temp_precip_models)

# Check for zero input fields in the radar, nowcast and NWP data.
# decide based on latest input file only
self.__params.zero_precip_radar = check_norain(
self.__precip,
self.__precip[-1],
self.__params.precip_threshold,
self.__config.norain_threshold,
self.__params.noise_kwargs["win_fun"],
)
# Set all inputs to 0 (also previous times)
# not strictly necessary but to be consistent with checking only
# latest observation rain field to set zero_precip_radar
if self.__params.zero_precip_radar:
self.__precip = np.where(
np.isfinite(self.__precip),
np.ones(self.__precip.shape) * np.nanmin(self.__precip),
self.__precip,
)

# The norain fraction threshold used for nwp is the default value of 0.0,
# since nwp does not suffer from clutter.
Expand Down Expand Up @@ -1481,10 +1491,15 @@ def __estimate_ar_parameters_radar(self):

# Finally, transpose GAMMA to ensure that the shape is the same as np.empty((n_cascade_levels, ar_order))
GAMMA = GAMMA.transpose()
assert GAMMA.shape == (
if len(GAMMA.shape) == 1:
GAMMA = GAMMA.reshape((GAMMA.size, 1))
if not GAMMA.shape == (
self.__config.n_cascade_levels,
self.__config.ar_order,
)
):
raise ValueError(
f"GAMMA shape {GAMMA.shape} does not match {self.__config.n_cascade_levels} cascade levels and ar_order of {self.__config.ar_order}"
)

# Print the GAMMA value
nowcast_utils.print_corrcoefs(GAMMA)
Expand Down Expand Up @@ -1629,13 +1644,22 @@ def __prepare_forecast_loop(self):
)

# initizalize the current and previous extrapolation forecast scale for the nowcasting component
# phi1 / (1 - phi2), see BPS2004
# ar_order=1: rho = phi1
# ar_order=2: rho = phi1 / (1 - phi2)
# -> see Hamilton1994, Pulkkinen2019, BPS2004 (Yule-Walker equations)
self.__state.rho_extrap_cascade_prev = np.repeat(
1.0, self.__params.PHI.shape[0]
)
self.__state.rho_extrap_cascade = self.__params.PHI[:, 0] / (
1.0 - self.__params.PHI[:, 1]
)
if self.__config.ar_order == 1:
self.__state.rho_extrap_cascade = self.__params.PHI[:, 0]
elif self.__config.ar_order == 2:
self.__state.rho_extrap_cascade = self.__params.PHI[:, 0] / (
1.0 - self.__params.PHI[:, 1]
)
else:
raise ValueError(
"Autoregression of higher order than 2 is not defined. Please set ar_order = 2."
)

def __initialize_noise_cascades(self):
"""
Expand Down Expand Up @@ -2037,6 +2061,7 @@ def __determine_skill_for_current_timestep(self, t):
PHI=self.__params.PHI,
correlations=self.__state.rho_extrap_cascade,
correlations_prev=self.__state.rho_extrap_cascade_prev,
ar_order=self.__config.ar_order,
)

def __determine_NWP_skill_for_next_timestep(self, t, j, worker_state):
Expand Down Expand Up @@ -2228,9 +2253,12 @@ def __regress_extrapolation_and_noise_cascades(self, j, worker_state, t):
)
)
# Renormalize the cascade
worker_state.precip_cascades[j][i][1] /= np.std(
worker_state.precip_cascades[j][i][1]
)
for nt, obs_cascade_timestep in enumerate(
worker_state.precip_cascades[j][i]
):
worker_state.precip_cascades[j][i][nt] /= np.std(
obs_cascade_timestep
)
else:
# use the deterministic AR(p) model computed above if
# perturbations are disabled
Expand Down Expand Up @@ -3625,6 +3653,20 @@ def forecast(
turns out to be a warranted functionality.
"""

# Check the input precip and ar_order to be consistent
# zero-precip/constant field in previous time steps has to be removed
# (constant field causes autoregression to fail)
precip, ar_order = check_previous_radar_obs(
precip,
ar_order,
{
"precip_thr": precip_thr,
"norain_thr": norain_thr,
# Set default window function to "tukey" to match the default in check_inputs() below
"win_fun": "tukey" if noise_kwargs is None else noise_kwargs["win_fun"],
Copy link
Copy Markdown
Contributor

@FelixE91 FelixE91 Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not totally sure whether this check should be for the entire domain or for the window function that is used in later noise generation.
Here I set it to match this window function ("tukey" by default).
If you think it is better to check the full domain at this point of the code, we simply delete lines 3665/3666.

},
)

blending_config = StepsBlendingConfig(
n_ens_members=n_ens_members,
n_cascade_levels=n_cascade_levels,
Expand Down
26 changes: 24 additions & 2 deletions pysteps/nowcasts/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
)
from pysteps.postprocessing import probmatching
from pysteps.timeseries import autoregression, correlation
from pysteps.utils.check_norain import check_norain
from pysteps.utils.check_norain import check_norain, check_previous_radar_obs

try:
import dask
Expand Down Expand Up @@ -358,11 +358,19 @@ def compute_forecast(self):
self.__precip = self.__precip[-(self.__config.ar_order + 1) :, :, :].copy()
self.__initialize_nowcast_components()
if check_norain(
self.__precip,
self.__precip[-1],
self.__config.precip_threshold,
self.__config.norain_threshold,
self.__params.noise_kwargs["win_fun"],
):
# Set all to inputs to 0 (also previous times) as we just check latest input in check_norain
self.__precip = self.__precip.copy()
self.__precip = np.where(
np.isfinite(self.__precip),
np.ones(self.__precip.shape) * np.nanmin(self.__precip),
self.__precip,
)

return zero_precipitation_forecast(
self.__config.n_ens_members,
self.__time_steps,
Expand Down Expand Up @@ -1495,6 +1503,20 @@ def forecast(
:cite:`Seed2003`, :cite:`BPS2006`, :cite:`SPN2013`, :cite:`PCH2019b`
"""

# Check the input precip and ar_order to be consistent
# zero-precip/constant field in previous time steps has to be removed
# (constant field causes autoregression to fail)
precip, ar_order = check_previous_radar_obs(
precip,
ar_order,
{
"precip_thr": precip_thr,
"norain_thr": norain_thr,
# Set default window function to "tukey" to match the default in check_inputs() below
"win_fun": "tukey" if noise_kwargs is None else noise_kwargs["win_fun"],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in blending.steps.py.
If we want to check the entire domain here, we can delete lines 1515/1516.

},
)

nowcaster_config = StepsNowcasterConfig(
n_ens_members=n_ens_members,
n_cascade_levels=n_cascade_levels,
Expand Down
Loading
Loading