Skip to content

Commit 33d80d4

Browse files
pulkkinsdneriniRubenImhoff
authored
Nowcast refactoring (#291)
* Remove unnecessary encoding lines * Change variable names to improve PEP 8 compatibility * Use 4 spaces in docstring indentations * Implement a common utility function for computing the S-PROG mask * Fix incorrect imports * Use f-strings instead of old-style printing * Implement utility method for computing nowcasts with non-integer time steps * Use state model for the nowcast loop function * Fix typos * Replace R_thr keyword argument with precip_thr * Remove extra index increment leading to possible IndexError * Use np.abs instead of abs * Minor docstring revisions * Fix typo * Implement supplying additional parameters to the state functions * Argument refactoring * Fix incorrect indices * Implement S-PROG by using utils.nowcast_main_loop * Add comment lines * Fix measurement of main loop time * Use PEP 8-compatible variable names * Remove unnecessary code lines * Use PEP 8-compatible variable names * Use PEP 8-compatible variable names in _ckeck_inputs * Move worker definition outside the nowcast loop * Supply parameters directly instead of dictionary * Use lowercase variable name for phi * Remove unnecessary code lines * Simplify code * Use a single update/forecast function for the nowcast loop * Use state and params dictionaries with the update method * First version of nowcast_main_loop with support for ensembles and velocity perturbations * Multiple fixes * Use dask for parallelization * Remove unnecessary encoding line * Use more descriptive variable name for rain rate * Refactored version of ANVILthat uses utils.nowcast_main_loop * Use proper check for deterministic/ensemble nowcast * Add return_output option to nowcast_main_loop * Docstring polishing * Implement support for callback functions in nowcast_main_loop * Docstring update * Bug fixes * Fix incorrect order of code lines * Fix incorrect initialization of starttime * Refactored STEPS module using utils.nowcast_main_loop * Remove unnecessary encoding line * Add checks for ensemble size and number of workers * Revise variable names to be compatible with the other nowcasting methods * Ensure that return_displacement is set to True * Remove unnecessary lines * First version of refactored LINDA using utils.nowcast_main_loop * Update docstring of the callback option * Implement callback and return_output options * Change order of input arguments * Fix parallelization bug * Add ensemble size check * Change num_ens_members to n_ens_members for compatibility with steps * Add test for callback function * Update black to newest version * Run black * Fix possible division by zero * Set allow_nonfinite_values automatically based on the input data * Replace None default argument with dict * Fix typo * Fix typo * Use float mask as in steps * Implement common utility method for incremental mask * Explicitly check that precip_thr is given * Explicitly check that precip_thr is specified when it is required * Remove redundant code * Use different name to avoid redefining built-in 'filter' * Remove unused variable * Replace filter with bp_filter * Use None default argument value instead of dict() * Change worker function names to avoid redefinition * Fix typo Co-authored-by: Ruben Imhoff <31476760+RubenImhoff@users.noreply.github.com> * Increase test coverage for anvil * Remove unnecessary encoding line * Improve variable naming in stack_cascades * Replace R with precip * Use consistent variable naming for forecasts * Run black * Remove unnecessary loop indices * Minor refactoring of printing functions * Use more descriptive variable name for velocity perturbators * Minor refactoring of seed generation * Use lowercase variable names for masks * Remove TODO comment * Use more decriptive variable names * Use lowercase variable names for gamma and psi * Use longer variable name for velocity perturbation generator * Replace precip_c and precip_d with more descriptive names * Replace precip_cascade with precip_cascades * Remove extra underscore from vp_ * Use longer names for lambda functions * Replace P with pert_gen * Replace pp with pert_gen * List all methods in the autosummary * Docstring addition * Fix incorrect initialization of precip_forecast_prev * Explicitly require nowcast type and ensemble size in nowcast_main_loop * First version of tests for nowcasts.utils * Add docstring * Remove unused metadata * Add future warnings for new argument names; support old names till 1.8.0 * Remoe generic filter for warnings * Pin micromamba to 0.24 since 0.25 fails with windows Co-authored-by: Daniele Nerini <daniele.nerini@gmail.com> Co-authored-by: Ruben Imhoff <31476760+RubenImhoff@users.noreply.github.com> Co-authored-by: ned <daniele.nerini@meteoswiss.ch>
1 parent c360858 commit 33d80d4

19 files changed

+1971
-1506
lines changed

.github/workflows/test_pysteps.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
- name: Install mamba and create environment
3737
uses: mamba-org/provision-with-micromamba@main
3838
with:
39+
micromamba-version: 0.24.0
3940
environment-file: ci/ci_test_env.yml
4041
environment-name: test_environment
4142
extra-specs: python=${{ matrix.python-version }}

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/psf/black
3-
rev: 22.3.0
3+
rev: 22.6.0
44
hooks:
55
- id: black
66
language_version: python3

pysteps/decorators.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""
1717
import inspect
1818
import uuid
19+
import warnings
1920
from collections import defaultdict
2021
from functools import wraps
2122

@@ -282,3 +283,35 @@ def _func_with_cache(*args, **kwargs):
282283
return _func_with_cache
283284

284285
return _memoize
286+
287+
288+
def deprecate_args(old_new_args, deprecation_release):
289+
"""
290+
Support deprecated argument names while issuing deprecation warnings.
291+
292+
Parameters
293+
----------
294+
old_new_args: dict[str, str]
295+
Mapping from old to new argument names.
296+
deprecation_release: str
297+
Specify which future release will convert this warning into an error.
298+
"""
299+
300+
def _deprecate(func):
301+
@wraps(func)
302+
def wrapper(*args, **kwargs):
303+
kwargs_names = list(kwargs.keys())
304+
for key_old in kwargs_names:
305+
if key_old in old_new_args:
306+
key_new = old_new_args[key_old]
307+
kwargs[key_new] = kwargs.pop(key_old)
308+
warnings.warn(
309+
f"Argument '{key_old}' has been renamed to '{key_new}'. "
310+
f"This will raise a TypeError in pysteps {deprecation_release}.",
311+
FutureWarning,
312+
)
313+
return func(*args, **kwargs)
314+
315+
return wrapper
316+
317+
return _deprecate

pysteps/nowcasts/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""Implementations of deterministic and ensemble nowcasting methods."""
32

43
from pysteps.nowcasts.interface import get_method

pysteps/nowcasts/anvil.py

Lines changed: 79 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""
32
pysteps.nowcasts.anvil
43
======================
@@ -23,7 +22,7 @@
2322
import numpy as np
2423
from scipy.ndimage import gaussian_filter
2524
from pysteps import cascade, extrapolation
26-
from pysteps.nowcasts import utils as nowcast_utils
25+
from pysteps.nowcasts.utils import nowcast_main_loop
2726
from pysteps.timeseries import autoregression
2827
from pysteps import utils
2928

@@ -149,36 +148,36 @@ def forecast(
149148
if filter_kwargs is None:
150149
filter_kwargs = dict()
151150

152-
print("Computing ANVIL nowcast:")
153-
print("------------------------")
151+
print("Computing ANVIL nowcast")
152+
print("-----------------------")
154153
print("")
155154

156-
print("Inputs:")
157-
print("-------")
158-
print("input dimensions: %dx%d" % (vil.shape[1], vil.shape[2]))
155+
print("Inputs")
156+
print("------")
157+
print(f"input dimensions: {vil.shape[1]}x{vil.shape[2]}")
159158
print("")
160159

161-
print("Methods:")
162-
print("--------")
163-
print("extrapolation: %s" % extrap_method)
164-
print("FFT: %s" % fft_method)
160+
print("Methods")
161+
print("-------")
162+
print(f"extrapolation: {extrap_method}")
163+
print(f"FFT: {fft_method}")
165164
print("")
166165

167-
print("Parameters:")
168-
print("-----------")
166+
print("Parameters")
167+
print("----------")
169168
if isinstance(timesteps, int):
170-
print("number of time steps: %d" % timesteps)
169+
print(f"number of time steps: {timesteps}")
171170
else:
172-
print("time steps: %s" % timesteps)
173-
print("parallel threads: %d" % num_workers)
174-
print("number of cascade levels: %d" % n_cascade_levels)
175-
print("order of the ARI(p,1) model: %d" % ar_order)
171+
print(f"time steps: {timesteps}")
172+
print(f"parallel threads: {num_workers}")
173+
print(f"number of cascade levels: {n_cascade_levels}")
174+
print(f"order of the ARI(p,1) model: {ar_order}")
176175
if type(ar_window_radius) == int:
177-
print("ARI(p,1) window radius: %d" % ar_window_radius)
176+
print(f"ARI(p,1) window radius: {ar_window_radius}")
178177
else:
179178
print("ARI(p,1) window radius: none")
180179

181-
print("R(VIL) window radius: %d" % r_vil_window_radius)
180+
print(f"R(VIL) window radius: {r_vil_window_radius}")
182181

183182
if measure_time:
184183
starttime_init = time.time()
@@ -188,11 +187,15 @@ def forecast(
188187

189188
if rainrate is None and apply_rainrate_mask:
190189
rainrate_mask = vil[-1, :] < 0.1
190+
else:
191+
rainrate_mask = None
191192

192193
if rainrate is not None:
193194
# determine the coefficients fields of the relation R=a*VIL+b by
194195
# localized linear regression
195196
r_vil_a, r_vil_b = _r_vil_regression(vil[-1, :], rainrate, r_vil_window_radius)
197+
else:
198+
r_vil_a, r_vil_b = None, None
196199

197200
# transform the input fields to Lagrangian coordinates by extrapolation
198201
extrapolator = extrapolation.get_method(extrap_method)
@@ -287,129 +290,41 @@ def worker(vil, i):
287290

288291
print("Starting nowcast computation.")
289292

290-
if measure_time:
291-
starttime_mainloop = time.time()
292-
293-
r_f = []
294-
295-
if isinstance(timesteps, int):
296-
timesteps = range(timesteps + 1)
297-
timestep_type = "int"
298-
else:
299-
original_timesteps = [0] + list(timesteps)
300-
timesteps = nowcast_utils.binned_timesteps(original_timesteps)
301-
timestep_type = "list"
293+
rainrate_f = []
302294

303-
if rainrate is not None:
304-
r_f_prev = r_vil_a * vil[-1, :] + r_vil_b
305-
else:
306-
r_f_prev = vil[-1, :]
307295
extrap_kwargs["return_displacement"] = True
308296

309-
dp = None
310-
t_prev = 0.0
311-
312-
for t, subtimestep_idx in enumerate(timesteps):
313-
if timestep_type == "list":
314-
subtimesteps = [original_timesteps[t_] for t_ in subtimestep_idx]
315-
else:
316-
subtimesteps = [t]
317-
318-
if (timestep_type == "list" and subtimesteps) or (
319-
timestep_type == "int" and t > 0
320-
):
321-
is_nowcast_time_step = True
322-
else:
323-
is_nowcast_time_step = False
324-
325-
if is_nowcast_time_step:
326-
print(
327-
"Computing nowcast for time step %d... " % t,
328-
end="",
329-
flush=True,
330-
)
331-
332-
if measure_time:
333-
starttime = time.time()
334-
335-
# iterate the ARI models for each cascade level
336-
for i in range(n_cascade_levels):
337-
vil_dec[i, :] = autoregression.iterate_ar_model(vil_dec[i, :], phi[i])
338-
339-
# recompose the cascade to obtain the forecast field
340-
vil_dec_dict = {}
341-
vil_dec_dict["cascade_levels"] = vil_dec[:, -1, :]
342-
vil_dec_dict["domain"] = "spatial"
343-
vil_dec_dict["normalized"] = False
344-
vil_f = recomp_method(vil_dec_dict)
345-
vil_f[~mask] = np.nan
346-
347-
if rainrate is not None:
348-
# convert VIL to rain rate
349-
r_f_new = r_vil_a * vil_f + r_vil_b
350-
else:
351-
r_f_new = vil_f
352-
if apply_rainrate_mask:
353-
r_f_new[rainrate_mask] = 0.0
354-
355-
r_f_new[r_f_new < 0.0] = 0.0
356-
357-
# advect the recomposed field to obtain the forecast for the current
358-
# time step (or subtimesteps if non-integer time steps are given)
359-
for t_sub in subtimesteps:
360-
if t_sub > 0:
361-
t_diff_prev_int = t_sub - int(t_sub)
362-
if t_diff_prev_int > 0.0:
363-
r_f_ip = (
364-
1.0 - t_diff_prev_int
365-
) * r_f_prev + t_diff_prev_int * r_f_new
366-
else:
367-
r_f_ip = r_f_prev
368-
369-
t_diff_prev = t_sub - t_prev
370-
371-
extrap_kwargs["displacement_prev"] = dp
372-
extrap_kwargs["allow_nonfinite_values"] = (
373-
True if np.any(~np.isfinite(r_f_ip)) else False
374-
)
375-
376-
r_f_ep, dp = extrapolator(
377-
r_f_ip,
378-
velocity,
379-
[t_diff_prev],
380-
**extrap_kwargs,
381-
)
382-
r_f.append(r_f_ep[0])
383-
t_prev = t_sub
384-
385-
# advect the forecast field by one time step if no subtimesteps in the
386-
# current interval were found
387-
if not subtimesteps:
388-
t_diff_prev = t + 1 - t_prev
389-
extrap_kwargs["displacement_prev"] = dp
390-
_, dp = extrapolator(
391-
None,
392-
velocity,
393-
[t_diff_prev],
394-
**extrap_kwargs,
395-
)
396-
t_prev = t + 1
397-
398-
r_f_prev = r_f_new
399-
400-
if is_nowcast_time_step:
401-
if measure_time:
402-
print("%.2f seconds." % (time.time() - starttime))
403-
else:
404-
print("done.")
405-
297+
state = {"vil_dec": vil_dec}
298+
params = {
299+
"apply_rainrate_mask": apply_rainrate_mask,
300+
"mask": mask,
301+
"n_cascade_levels": n_cascade_levels,
302+
"phi": phi,
303+
"rainrate": rainrate,
304+
"rainrate_mask": rainrate_mask,
305+
"recomp_method": recomp_method,
306+
"r_vil_a": r_vil_a,
307+
"r_vil_b": r_vil_b,
308+
}
309+
310+
rainrate_f = nowcast_main_loop(
311+
vil[-1, :],
312+
velocity,
313+
state,
314+
timesteps,
315+
extrap_method,
316+
_update,
317+
extrap_kwargs=extrap_kwargs,
318+
params=params,
319+
measure_time=measure_time,
320+
)
406321
if measure_time:
407-
mainloop_time = time.time() - starttime_mainloop
322+
rainrate_f, mainloop_time = rainrate_f
408323

409324
if measure_time:
410-
return np.stack(r_f), init_time, mainloop_time
325+
return np.stack(rainrate_f), init_time, mainloop_time
411326
else:
412-
return np.stack(r_f)
327+
return np.stack(rainrate_f)
413328

414329

415330
def _check_inputs(vil, rainrate, velocity, timesteps, ar_order):
@@ -559,3 +474,31 @@ def _r_vil_regression(vil, r, window_radius):
559474
b[~mask_vil] = 0.0
560475

561476
return a, b
477+
478+
479+
def _update(state, params):
480+
# iterate the ARI models for each cascade level
481+
for i in range(params["n_cascade_levels"]):
482+
state["vil_dec"][i, :] = autoregression.iterate_ar_model(
483+
state["vil_dec"][i, :], params["phi"][i]
484+
)
485+
486+
# recompose the cascade to obtain the forecast field
487+
vil_dec_dict = {}
488+
vil_dec_dict["cascade_levels"] = state["vil_dec"][:, -1, :]
489+
vil_dec_dict["domain"] = "spatial"
490+
vil_dec_dict["normalized"] = False
491+
vil_f = params["recomp_method"](vil_dec_dict)
492+
vil_f[~params["mask"]] = np.nan
493+
494+
if params["rainrate"] is not None:
495+
# convert VIL to rain rate
496+
rainrate_f_new = params["r_vil_a"] * vil_f + params["r_vil_b"]
497+
else:
498+
rainrate_f_new = vil_f
499+
if params["apply_rainrate_mask"]:
500+
rainrate_f_new[params["rainrate_mask"]] = 0.0
501+
502+
rainrate_f_new[rainrate_f_new < 0.0] = 0.0
503+
504+
return rainrate_f_new, state

pysteps/nowcasts/extrapolation.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""
32
pysteps.nowcasts.extrapolation
43
==============================
@@ -34,24 +33,24 @@ def forecast(
3433
Parameters
3534
----------
3635
precip: array-like
37-
Two-dimensional array of shape (m,n) containing the input precipitation
38-
field.
36+
Two-dimensional array of shape (m,n) containing the input precipitation
37+
field.
3938
velocity: array-like
40-
Array of shape (2,m,n) containing the x- and y-components of the
41-
advection field. The velocities are assumed to represent one time step
42-
between the inputs.
39+
Array of shape (2,m,n) containing the x- and y-components of the
40+
advection field. The velocities are assumed to represent one time step
41+
between the inputs.
4342
timesteps: int or list of floats
44-
Number of time steps to forecast or a list of time steps for which the
45-
forecasts are computed (relative to the input time step). The elements of
46-
the list are required to be in ascending order.
43+
Number of time steps to forecast or a list of time steps for which the
44+
forecasts are computed (relative to the input time step). The elements
45+
of the list are required to be in ascending order.
4746
extrap_method: str, optional
48-
Name of the extrapolation method to use. See the documentation of
49-
pysteps.extrapolation.interface.
47+
Name of the extrapolation method to use. See the documentation of
48+
pysteps.extrapolation.interface.
5049
extrap_kwargs: dict, optional
51-
Optional dictionary that is expanded into keyword arguments for the
52-
extrapolation method.
50+
Optional dictionary that is expanded into keyword arguments for the
51+
extrapolation method.
5352
measure_time: bool, optional
54-
If True, measure, print, and return the computation time.
53+
If True, measure, print, and return the computation time.
5554
5655
Returns
5756
-------

pysteps/nowcasts/interface.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""
32
pysteps.nowcasts.interface
43
==========================

pysteps/nowcasts/lagrangian_probability.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# -*- coding: utf-8 -*-
21
"""
32
pysteps.nowcasts.lagrangian_probability
43
=======================================

0 commit comments

Comments
 (0)