Skip to content

Commit 1b49030

Browse files
committed
import directly to xarray and rewrite utils_transformation to work with it
1 parent 917c83b commit 1b49030

13 files changed

+754
-319
lines changed

ci/ci_test_env.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies:
1818
- pillow
1919
- pyproj
2020
- scipy
21+
- xarray
2122
# Optional dependencies
2223
- dask
2324
- pyfftw

pysteps/converters.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
pysteps.converters
4+
==================
5+
6+
Module with data converter functions.
7+
8+
.. autosummary::
9+
:toctree: ../generated/
10+
11+
convert_to_xarray_dataset
12+
"""
13+
14+
import numpy as np
15+
import pyproj
16+
import xarray as xr
17+
18+
from pysteps.utils.conversion import cf_parameters_from_unit
19+
20+
# TODO(converters): Write methods for converting Proj.4 projection definitions
21+
# into CF grid mapping attributes. Currently this has been implemented for
22+
# the stereographic projection.
23+
# The conversions implemented here are take from:
24+
# https://github.com/cf-convention/cf-convention.github.io/blob/master/wkt-proj-4.md
25+
26+
27+
def _convert_proj4_to_grid_mapping(proj4str):
28+
tokens = proj4str.split("+")
29+
30+
d = {}
31+
for t in tokens[1:]:
32+
t = t.split("=")
33+
if len(t) > 1:
34+
d[t[0]] = t[1].strip()
35+
36+
params = {}
37+
# TODO(exporters): implement more projection types here
38+
if d["proj"] == "stere":
39+
grid_mapping_var_name = "polar_stereographic"
40+
grid_mapping_name = "polar_stereographic"
41+
v = d["lon_0"] if d["lon_0"][-1] not in ["E", "W"] else d["lon_0"][:-1]
42+
params["straight_vertical_longitude_from_pole"] = float(v)
43+
v = d["lat_0"] if d["lat_0"][-1] not in ["N", "S"] else d["lat_0"][:-1]
44+
params["latitude_of_projection_origin"] = float(v)
45+
if "lat_ts" in list(d.keys()):
46+
params["standard_parallel"] = float(d["lat_ts"])
47+
elif "k_0" in list(d.keys()):
48+
params["scale_factor_at_projection_origin"] = float(d["k_0"])
49+
params["false_easting"] = float(d["x_0"])
50+
params["false_northing"] = float(d["y_0"])
51+
elif d["proj"] == "aea": # Albers Conical Equal Area
52+
grid_mapping_var_name = "proj"
53+
grid_mapping_name = "albers_conical_equal_area"
54+
params["false_easting"] = float(d["x_0"]) if "x_0" in d else float(0)
55+
params["false_northing"] = float(d["y_0"]) if "y_0" in d else float(0)
56+
v = d["lon_0"] if "lon_0" in d else float(0)
57+
params["longitude_of_central_meridian"] = float(v)
58+
v = d["lat_0"] if "lat_0" in d else float(0)
59+
params["latitude_of_projection_origin"] = float(v)
60+
v1 = d["lat_1"] if "lat_1" in d else float(0)
61+
v2 = d["lat_2"] if "lat_2" in d else float(0)
62+
params["standard_parallel"] = (float(v1), float(v2))
63+
else:
64+
print("unknown projection", d["proj"])
65+
return None, None, None
66+
67+
return grid_mapping_var_name, grid_mapping_name, params
68+
69+
70+
def convert_to_xarray_dataset(
71+
precip: np.ndarray,
72+
quality: np.ndarray | None,
73+
metadata: dict[str, str | float | None],
74+
) -> xr.Dataset:
75+
"""
76+
Read a precip, quality, metadata tuple as returned by the importers
77+
(:py:mod:`pysteps.io.importers`) and return an xarray dataset containing
78+
this data.
79+
80+
Parameters
81+
----------
82+
precip: array
83+
2D array containing imported precipitation data.
84+
quality: array, None
85+
2D array containing the quality values of the imported precipitation
86+
data, can be None.
87+
metadata: dict
88+
Metadata dictionary containing the attributes described in the
89+
documentation of :py:mod:`pysteps.io.importers`.
90+
91+
Returns
92+
-------
93+
out: Dataset
94+
A CF compliant xarray dataset, which contains all data and metadata.
95+
96+
"""
97+
var_name, attrs = cf_parameters_from_unit(metadata["unit"])
98+
h, w = precip.shape
99+
x_r = np.linspace(metadata["x1"], metadata["x2"], w + 1)[:-1]
100+
x_r += 0.5 * (x_r[1] - x_r[0])
101+
y_r = np.linspace(metadata["y1"], metadata["y2"], h + 1)[:-1]
102+
y_r += 0.5 * (y_r[1] - y_r[0])
103+
104+
# flip yr vector if yorigin is upper
105+
if metadata["yorigin"] == "upper":
106+
y_r = np.flip(y_r)
107+
108+
x_2d, y_2d = np.meshgrid(x_r, y_r)
109+
pr = pyproj.Proj(metadata["projection"])
110+
lon, lat = pr(x_2d.flatten(), y_2d.flatten(), inverse=True)
111+
112+
(
113+
grid_mapping_var_name,
114+
grid_mapping_name,
115+
grid_mapping_params,
116+
) = _convert_proj4_to_grid_mapping(metadata["projection"])
117+
118+
data_vars = {
119+
var_name: (
120+
["y", "x"],
121+
precip,
122+
{
123+
"units": attrs["units"],
124+
"standard_name": attrs["standard_name"],
125+
"long_name": attrs["long_name"],
126+
"grid_mapping": "projection",
127+
"transform": metadata["transform"],
128+
"accutime": metadata["accutime"],
129+
"threshold": metadata["threshold"],
130+
"zerovalue": metadata["zerovalue"],
131+
"zr_a": metadata["zr_a"],
132+
"zr_b": metadata["zr_b"],
133+
},
134+
)
135+
}
136+
if quality is not None:
137+
data_vars["quality"] = (
138+
["y", "x"],
139+
quality,
140+
{
141+
"units": "1",
142+
"standard_name": "quality_flag",
143+
"grid_mapping": "projection",
144+
},
145+
)
146+
coords = {
147+
"y": (
148+
["y"],
149+
y_r,
150+
{
151+
"axis": "Y",
152+
"long_name": "y-coordinate in Cartesian system",
153+
"standard_name": "projection_y_coordinate",
154+
"units": metadata["cartesian_unit"],
155+
},
156+
),
157+
"x": (
158+
["x"],
159+
x_r,
160+
{
161+
"axis": "X",
162+
"long_name": "x-coordinate in Cartesian system",
163+
"standard_name": "projection_x_coordinate",
164+
"units": metadata["cartesian_unit"],
165+
},
166+
),
167+
"lon": (
168+
["y", "x"],
169+
lon.reshape(precip.shape),
170+
{
171+
"long_name": "longitude coordinate",
172+
"standard_name": "longitude",
173+
# TODO(converters): Don't hard-code the unit.
174+
"units": "degrees_east",
175+
},
176+
),
177+
"lat": (
178+
["y", "x"],
179+
lat.reshape(precip.shape),
180+
{
181+
"long_name": "latitude coordinate",
182+
"standard_name": "latitude",
183+
# TODO(converters): Don't hard-code the unit.
184+
"units": "degrees_north",
185+
},
186+
),
187+
}
188+
if grid_mapping_var_name is not None:
189+
coords[grid_mapping_name] = (
190+
(
191+
[],
192+
None,
193+
{"grid_mapping_name": grid_mapping_name, **grid_mapping_params},
194+
),
195+
)
196+
attrs = {
197+
"Conventions": "CF-1.7",
198+
"institution": metadata["institution"],
199+
"projection": metadata["projection"],
200+
"precip_var": var_name,
201+
}
202+
return xr.Dataset(data_vars=data_vars, coords=coords, attrs=attrs)

pysteps/decorators.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import numpy as np
2424

25+
from pysteps.converters import convert_to_xarray_dataset
26+
2527

2628
def _add_extra_kwrds_to_docstrings(target_func, extra_kwargs_doc_text):
2729
"""
@@ -66,7 +68,7 @@ def postprocess_import(fillna=np.nan, dtype="double"):
6668
def _postprocess_import(importer):
6769
@wraps(importer)
6870
def _import_with_postprocessing(*args, **kwargs):
69-
precip, *other_args = importer(*args, **kwargs)
71+
precip, quality, metadata = importer(*args, **kwargs)
7072

7173
_dtype = kwargs.get("dtype", dtype)
7274

@@ -88,7 +90,7 @@ def _import_with_postprocessing(*args, **kwargs):
8890
mask = ~np.isfinite(precip)
8991
precip[mask] = _fillna
9092

91-
return (precip.astype(_dtype),) + tuple(other_args)
93+
return convert_to_xarray_dataset(precip.astype(_dtype), quality, metadata)
9294

9395
extra_kwargs_doc = """
9496
Other Parameters

pysteps/io/importers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,10 @@
8989
from functools import partial
9090

9191
import numpy as np
92-
9392
from matplotlib.pyplot import imread
9493

9594
from pysteps.decorators import postprocess_import
96-
from pysteps.exceptions import DataModelError
97-
from pysteps.exceptions import MissingOptionalDependency
95+
from pysteps.exceptions import DataModelError, MissingOptionalDependency
9896
from pysteps.utils import aggregate_fields
9997

10098
try:

pysteps/io/readers.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
"""
1313

1414
import numpy as np
15+
import xarray as xr
1516

1617

17-
def read_timeseries(inputfns, importer, **kwargs):
18+
def read_timeseries(inputfns, importer, **kwargs) -> xr.Dataset | None:
1819
"""
1920
Read a time series of input files using the methods implemented in the
20-
:py:mod:`pysteps.io.importers` module and stack them into a 3d array of
21-
shape (num_timesteps, height, width).
21+
:py:mod:`pysteps.io.importers` module and stack them into a 3d xarray
22+
dataset of shape (num_timesteps, height, width).
2223
2324
Parameters
2425
----------
@@ -32,50 +33,50 @@ def read_timeseries(inputfns, importer, **kwargs):
3233
3334
Returns
3435
-------
35-
out: tuple
36-
A three-element tuple containing the read data and quality rasters and
36+
out: Dataset
37+
A dataset containing the read data and quality rasters and
3738
associated metadata. If an input file name is None, the corresponding
3839
precipitation and quality fields are filled with nan values. If all
3940
input file names are None or if the length of the file name list is
40-
zero, a three-element tuple containing None values is returned.
41+
zero, None is returned.
4142
4243
"""
4344

4445
# check for missing data
45-
precip_ref = None
46+
dataset_ref = None
4647
if all(ifn is None for ifn in inputfns):
47-
return None, None, None
48+
return None
4849
else:
4950
if len(inputfns[0]) == 0:
50-
return None, None, None
51+
return None
5152
for ifn in inputfns[0]:
5253
if ifn is not None:
53-
precip_ref, quality_ref, metadata = importer(ifn, **kwargs)
54+
dataset_ref = importer(ifn, **kwargs)
5455
break
5556

56-
if precip_ref is None:
57-
return None, None, None
57+
if dataset_ref is None:
58+
return None
5859

59-
precip = []
60-
quality = []
61-
timestamps = []
60+
startdate = min(inputfns[1])
61+
62+
datasets = []
6263
for i, ifn in enumerate(inputfns[0]):
6364
if ifn is not None:
64-
precip_, quality_, _ = importer(ifn, **kwargs)
65-
precip.append(precip_)
66-
quality.append(quality_)
67-
timestamps.append(inputfns[1][i])
65+
dataset_ = importer(ifn, **kwargs)
6866
else:
69-
precip.append(precip_ref * np.nan)
70-
if quality_ref is not None:
71-
quality.append(quality_ref * np.nan)
72-
else:
73-
quality.append(None)
74-
timestamps.append(inputfns[1][i])
75-
76-
# Replace this with stack?
77-
precip = np.concatenate([precip_[None, :, :] for precip_ in precip])
78-
# TODO: Q should be organized as R, but this is not trivial as Q_ can be also None or a scalar
79-
metadata["timestamps"] = np.array(timestamps)
67+
dataset_ = dataset_ref * np.nan
68+
dataset_ = dataset_.expand_dims(dim="time", axis=0)
69+
dataset_ = dataset_.assign_coords(
70+
time=(
71+
"time",
72+
[inputfns[1][i]],
73+
{
74+
"long_name": "forecast time",
75+
"units": f"seconds since {startdate:%Y-%m-%d %H:%M:%S}",
76+
},
77+
)
78+
)
79+
datasets.append(dataset_)
8080

81-
return precip, quality, metadata
81+
dataset = xr.concat(datasets, dim="time")
82+
return dataset

pysteps/nowcasts/anvil.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
"""
2020

2121
import time
22+
2223
import numpy as np
2324
from scipy.ndimage import gaussian_filter
24-
from pysteps import cascade, extrapolation
25+
26+
from pysteps import cascade, extrapolation, utils
2527
from pysteps.nowcasts.utils import nowcast_main_loop
2628
from pysteps.timeseries import autoregression
27-
from pysteps import utils
2829

2930
try:
3031
import dask

0 commit comments

Comments
 (0)