Skip to content

Commit 3436d27

Browse files
committed
Merge remote-tracking branch 'origin/xarray/main' into xarray/blend-utils
2 parents 2e35624 + d77f1b4 commit 3436d27

19 files changed

+347
-298
lines changed

environment_dev.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ dependencies:
2121
- dask
2222
- pyfftw
2323
- h5py
24-
- PyWavelets
2524
- pygrib
2625
- black
2726
- pytest-cov
@@ -32,3 +31,5 @@ dependencies:
3231
- pandas
3332
- rasterio
3433
- xarray
34+
- geotiff
35+
- cookiecutter

pysteps/blending/linear_blending.py

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,28 @@
2121
"""
2222

2323
import numpy as np
24+
import xarray as xr
2425
from pysteps import nowcasts
2526
from pysteps.utils import conversion
2627
from scipy.stats import rankdata
2728

29+
from pysteps.xarray_helpers import convert_output_to_xarray_dataset
30+
2831

2932
def forecast(
30-
precip,
31-
precip_metadata,
32-
velocity,
33+
radar_dataset: xr.Dataset,
3334
timesteps,
3435
timestep,
3536
nowcast_method,
36-
precip_nwp=None,
37-
precip_nwp_metadata=None,
37+
model_dataset: xr.Dataset = None,
3838
start_blending=120,
3939
end_blending=240,
4040
fill_nwp=True,
4141
saliency=False,
4242
nowcast_kwargs=None,
4343
):
4444
"""Generate a forecast by linearly or saliency-based blending of nowcasts with NWP data
45+
# XR: Update docstring
4546
4647
Parameters
4748
----------
@@ -105,31 +106,27 @@ def forecast(
105106
if nowcast_kwargs is None:
106107
nowcast_kwargs = dict()
107108

108-
# Ensure that only the most recent precip timestep is used
109-
if len(precip.shape) == 3:
110-
precip = precip[-1, :, :]
111-
112109
# First calculate the number of needed timesteps (up to end_blending) for the nowcast
113110
# to ensure that the nowcast calculation time is limited.
114111
timesteps_nowcast = int(end_blending / timestep)
115112

116113
nowcast_method_func = nowcasts.get_method(nowcast_method)
117114

118115
# Check if NWP data is given as input
119-
if precip_nwp is not None:
116+
if model_dataset is not None:
120117
# Calculate the nowcast
121-
precip_nowcast = nowcast_method_func(
122-
precip,
123-
velocity,
124-
timesteps_nowcast,
125-
**nowcast_kwargs,
118+
nowcast_dataset = nowcast_method_func(
119+
radar_dataset, timesteps_nowcast, **nowcast_kwargs
126120
)
127121

128122
# Make sure that precip_nowcast and precip_nwp are in mm/h
129-
precip_nowcast, _ = conversion.to_rainrate(
130-
precip_nowcast, metadata=precip_metadata
131-
)
132-
precip_nwp, _ = conversion.to_rainrate(precip_nwp, metadata=precip_nwp_metadata)
123+
nowcast_dataset = conversion.to_rainrate(nowcast_dataset)
124+
nowcast_precip_var = nowcast_dataset.attrs["precip_var"]
125+
precip_nowcast = nowcast_dataset[nowcast_precip_var].values
126+
127+
model_dataset = conversion.to_rainrate(model_dataset)
128+
model_precip_var = model_dataset.attrs["precip_var"]
129+
precip_nwp = model_dataset[model_precip_var].values
133130

134131
if len(precip_nowcast.shape) == 4:
135132
n_ens_members_nowcast = precip_nowcast.shape[0]
@@ -261,22 +258,19 @@ def forecast(
261258

262259
else:
263260
# Calculate the nowcast
264-
precip_nowcast = nowcast_method_func(
265-
precip,
266-
velocity,
267-
timesteps,
268-
**nowcast_kwargs,
261+
nowcast_dataset = nowcast_method_func(
262+
radar_dataset, timesteps, **nowcast_kwargs
269263
)
270264

271265
# Make sure that precip_nowcast and precip_nwp are in mm/h
272-
precip_nowcast, _ = conversion.to_rainrate(
273-
precip_nowcast, metadata=precip_metadata
274-
)
266+
nowcast_dataset = conversion.to_rainrate(nowcast_dataset)
267+
nowcast_precip_var = nowcast_dataset.attrs["precip_var"]
268+
precip_nowcast = nowcast_dataset[nowcast_precip_var].values
275269

276270
# If no NWP data is given, the blended field is simply equal to the nowcast field
277271
precip_blended = precip_nowcast
278272

279-
return precip_blended
273+
return convert_output_to_xarray_dataset(radar_dataset, timesteps, precip_blended)
280274

281275

282276
def _get_slice(n_dims, ref_dim, ref_id):
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import numpy as np
2+
import xarray as xr
3+
from pysteps.xarray_helpers import convert_output_to_xarray_dataset
4+
5+
6+
def extrapolate(precip_dataset: xr.Dataset, timesteps, outval=np.nan, **kwargs):
7+
"""
8+
A dummy extrapolation method to apply Eulerian persistence to a
9+
two-dimensional precipitation field. The method returns the a sequence
10+
of the same initial field with no extrapolation applied (i.e. Eulerian
11+
persistence).
12+
13+
Parameters
14+
----------
15+
precip_dataset : xarray.Dataset
16+
xarray dataset containing the input precipitation field. All
17+
values are required to be finite.
18+
timesteps : int or list of floats
19+
Number of time steps or a list of time steps.
20+
outval : float, optional
21+
Not used by the method.
22+
23+
Other Parameters
24+
----------------
25+
return_displacement : bool
26+
If True, return the total advection velocity (displacement) between the
27+
initial input field and the advected one integrated along
28+
the trajectory. Default : False
29+
30+
Returns
31+
-------
32+
out : array or tuple
33+
If return_displacement=False, return a sequence of the same initial field
34+
of shape (num_timesteps,m,n). Otherwise, return a tuple containing the
35+
replicated fields and a (2,m,n) array of zeros.
36+
37+
References
38+
----------
39+
:cite:`GZ2002`
40+
41+
"""
42+
del outval # Unused by _eulerian_persistence
43+
precip_var = precip_dataset.attrs["precip_var"]
44+
precip = precip_dataset[precip_var].values[-1]
45+
46+
if isinstance(timesteps, int):
47+
num_timesteps = timesteps
48+
else:
49+
num_timesteps = len(timesteps)
50+
51+
return_displacement = kwargs.get("return_displacement", False)
52+
53+
extrapolated_precip = np.repeat(precip[np.newaxis, :, :], num_timesteps, axis=0)
54+
extrapolated_precip_dataset = convert_output_to_xarray_dataset(
55+
precip_dataset, timesteps, extrapolated_precip
56+
)
57+
58+
if not return_displacement:
59+
return extrapolated_precip_dataset
60+
else:
61+
return extrapolated_precip_dataset, np.zeros((2,) + extrapolated_precip.shape)

pysteps/extrapolation/interface.py

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -34,63 +34,7 @@
3434
"""
3535

3636
import numpy as np
37-
38-
from pysteps.extrapolation import semilagrangian
39-
40-
41-
def eulerian_persistence(precip, velocity, timesteps, outval=np.nan, **kwargs):
42-
"""
43-
A dummy extrapolation method to apply Eulerian persistence to a
44-
two-dimensional precipitation field. The method returns the a sequence
45-
of the same initial field with no extrapolation applied (i.e. Eulerian
46-
persistence).
47-
48-
Parameters
49-
----------
50-
precip : array-like
51-
Array of shape (m,n) containing the input precipitation field. All
52-
values are required to be finite.
53-
velocity : array-like
54-
Not used by the method.
55-
timesteps : int or list of floats
56-
Number of time steps or a list of time steps.
57-
outval : float, optional
58-
Not used by the method.
59-
60-
Other Parameters
61-
----------------
62-
return_displacement : bool
63-
If True, return the total advection velocity (displacement) between the
64-
initial input field and the advected one integrated along
65-
the trajectory. Default : False
66-
67-
Returns
68-
-------
69-
out : array or tuple
70-
If return_displacement=False, return a sequence of the same initial field
71-
of shape (num_timesteps,m,n). Otherwise, return a tuple containing the
72-
replicated fields and a (2,m,n) array of zeros.
73-
74-
References
75-
----------
76-
:cite:`GZ2002`
77-
78-
"""
79-
del velocity, outval # Unused by _eulerian_persistence
80-
81-
if isinstance(timesteps, int):
82-
num_timesteps = timesteps
83-
else:
84-
num_timesteps = len(timesteps)
85-
86-
return_displacement = kwargs.get("return_displacement", False)
87-
88-
extrapolated_precip = np.repeat(precip[np.newaxis, :, :], num_timesteps, axis=0)
89-
90-
if not return_displacement:
91-
return extrapolated_precip
92-
else:
93-
return extrapolated_precip, np.zeros((2,) + extrapolated_precip.shape)
37+
from pysteps.extrapolation import semilagrangian, eulerian_persistence
9438

9539

9640
def _do_nothing(precip, velocity, timesteps, outval=np.nan, **kwargs):
@@ -105,7 +49,7 @@ def _return_none(**kwargs):
10549

10650

10751
_extrapolation_methods = dict()
108-
_extrapolation_methods["eulerian"] = eulerian_persistence
52+
_extrapolation_methods["eulerian"] = eulerian_persistence.extrapolate
10953
_extrapolation_methods["semilagrangian"] = semilagrangian.extrapolate
11054
_extrapolation_methods[None] = _do_nothing
11155
_extrapolation_methods["none"] = _do_nothing

pysteps/io/importers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2021,7 +2021,6 @@ def _read_hdf5_cont(f, d):
20212021
}
20222022

20232023
else:
2024-
20252024
# Save h5py.Dataset by group name
20262025
d[key] = np.array(value)
20272026

pysteps/nowcasts/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
_nowcast_methods = dict()
4545
_nowcast_methods["anvil"] = anvil.forecast
46-
_nowcast_methods["eulerian"] = eulerian_persistence
46+
_nowcast_methods["eulerian"] = eulerian_persistence.extrapolate
4747
_nowcast_methods["extrapolation"] = extrapolation.forecast
4848
_nowcast_methods["lagrangian"] = extrapolation.forecast
4949
_nowcast_methods["lagrangian_probability"] = lagrangian_probability.forecast

pysteps/nowcasts/steps.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ class StepsNowcasterConfig:
5555
Specifies the threshold value for minimum observable precipitation
5656
intensity. Required if mask_method is not None or conditional is True.
5757
norain_threshold: float
58-
Specifies the threshold value for the fraction of rainy (see above) pixels
59-
in the radar rainfall field below which we consider there to be no rain.
60-
Depends on the amount of clutter typically present.
61-
Standard set to 0.0
58+
Specifies the threshold value for the fraction of rainy (see above) pixels
59+
in the radar rainfall field below which we consider there to be no rain.
60+
Depends on the amount of clutter typically present.
61+
Standard set to 0.0
6262
kmperpixel: float, optional
6363
Spatial resolution of the input data (kilometers/pixel). Required if
6464
vel_pert_method is not None or mask_method is 'incremental'.

pysteps/pystepsrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"path_fmt": "%Y%m%d",
5252
"fn_pattern": "%Y%m%d%H%M_FINUTM",
5353
"fn_ext": "tif",
54-
"importer": "geotiff",
54+
"importer": "fmi_geotiff",
5555
"timestep": 5,
5656
"importer_kwargs": {}
5757
},

pysteps/tests/test_blending_linear_blending.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# -*- coding: utf-8 -*-
22

3+
from datetime import datetime
34
import numpy as np
45
import pytest
56
from pysteps.blending.linear_blending import forecast, _get_ranked_salience, _get_ws
67
from numpy.testing import assert_array_almost_equal
78
from pysteps.utils import transformation
9+
from pysteps.xarray_helpers import convert_input_to_xarray_dataset
810

911
# Test function arguments
1012
linear_arg_values = [
@@ -218,30 +220,68 @@ def test_linear_blending(
218220
for i in range(100, 200):
219221
r_input[:, i, :] = 11.0
220222
else:
221-
r_input = np.zeros((200, 200))
223+
r_input = np.zeros((1, 200, 200))
222224
for i in range(100, 200):
223-
r_input[i, :] = 11.0
225+
r_input[0, i, :] = 11.0
226+
227+
metadata = dict()
228+
metadata["unit"] = "mm/h"
229+
metadata["cartesian_unit"] = "km"
230+
metadata["accutime"] = 5.0
231+
metadata["zerovalue"] = 0.0
232+
metadata["threshold"] = 0.01
233+
metadata["zr_a"] = 200.0
234+
metadata["zr_b"] = 1.6
235+
metadata["x1"] = 0.0
236+
metadata["x2"] = 200.0
237+
metadata["y1"] = 0.0
238+
metadata["y2"] = 200.0
239+
metadata["yorigin"] = "lower"
240+
metadata["institution"] = "test"
241+
metadata["projection"] = (
242+
"+proj=lcc +lon_0=4.55 +lat_1=50.8 +lat_2=50.8 +a=6371229 +es=0 +lat_0=50.8 +x_0=365950 +y_0=-365950.000000001"
243+
)
244+
radar_dataset = convert_input_to_xarray_dataset(
245+
r_input,
246+
None,
247+
metadata,
248+
datetime.fromisoformat("2021-07-04T11:50:00.000000000"),
249+
300,
250+
)
224251

225252
# Transform from mm/h to dB
226-
r_input, _ = transformation.dB_transform(
227-
r_input, None, threshold=0.1, zerovalue=-15.0
253+
radar_dataset = transformation.dB_transform(
254+
radar_dataset, threshold=0.1, zerovalue=-15.0
228255
)
256+
if V is not None:
257+
radar_dataset["velocity_x"] = (["y", "x"], V[0])
258+
radar_dataset["velocity_y"] = (["y", "x"], V[1])
259+
260+
if r_nwp is None:
261+
model_dataset = None
262+
else:
263+
model_dataset = convert_input_to_xarray_dataset(
264+
r_nwp,
265+
None,
266+
metadata,
267+
datetime.fromisoformat("2021-07-04T11:50:00.000000000"),
268+
300,
269+
)
229270

230271
# Calculate the blended field
231-
r_blended = forecast(
232-
r_input,
233-
dict({"unit": "mm/h", "transform": "dB"}),
234-
V,
272+
blended_dataset = forecast(
273+
radar_dataset,
235274
n_timesteps,
236275
timestep,
237276
nowcast_method,
238-
r_nwp,
239-
dict({"unit": "mm/h"}),
277+
model_dataset,
240278
start_blending=start_blending,
241279
end_blending=end_blending,
242280
fill_nwp=fill_nwp,
243281
saliency=salient_blending,
244282
)
283+
blended_precip_var = blended_dataset.attrs["precip_var"]
284+
r_blended = blended_dataset[blended_precip_var].values
245285

246286
# Assert that the blended field has the expected dimension
247287
if n_models > 1:

0 commit comments

Comments
 (0)