grid-process 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- grid_process/__init__.py +14 -0
- grid_process/aggreg.py +447 -0
- grid_process/blending.py +206 -0
- grid_process/config/__init__.py +5 -0
- grid_process/config/grid_process.yaml +20 -0
- grid_process/config/library_settings.py +68 -0
- grid_process/da_utils.py +104 -0
- grid_process/data_model.py +531 -0
- grid_process/ds_utils.py +61 -0
- grid_process/ens_stats.py +396 -0
- grid_process/grid_interp.py +2016 -0
- grid_process/grid_interp_utils.py +208 -0
- grid_process/helpers.py +174 -0
- grid_process/optimization.py +50 -0
- grid_process/sun.py +502 -0
- grid_process/time_utils.py +110 -0
- grid_process/timeseries.py +547 -0
- grid_process/units_conv.py +209 -0
- grid_process-1.0.0.dist-info/METADATA +155 -0
- grid_process-1.0.0.dist-info/RECORD +23 -0
- grid_process-1.0.0.dist-info/WHEEL +4 -0
- grid_process-1.0.0.dist-info/licenses/AUTHORS +11 -0
- grid_process-1.0.0.dist-info/licenses/LICENSE +29 -0
grid_process/ds_utils.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
|
|
2
|
+
#
|
|
3
|
+
# Distributed under the terms of the BSD 3-Clause License.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Module with general utility functions that operate on xarray.Dataset
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
from collections.abc import Hashable
|
|
14
|
+
|
|
15
|
+
import xarray as xr
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ds_to_da(
|
|
21
|
+
ds: xr.Dataset,
|
|
22
|
+
) -> tuple[xr.DataArray, dict[Hashable, dict[Hashable, Any]]]:
|
|
23
|
+
"""
|
|
24
|
+
Transform a dataset containing model data and one or several data
|
|
25
|
+
variables to a dataarray with the variables stacked as a new dimension.
|
|
26
|
+
The function also returns the attributes of the data variables for further
|
|
27
|
+
use.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
ds : xarray.Dataset
|
|
32
|
+
contain one or more variables
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
da : xarray.DataArray
|
|
37
|
+
contains the data variables from the input dataset stacked in a new
|
|
38
|
+
`variable` dimension
|
|
39
|
+
vars_attrs : dictionary
|
|
40
|
+
contains the attributes of each data variables in the input dataset.
|
|
41
|
+
|
|
42
|
+
See also
|
|
43
|
+
--------
|
|
44
|
+
:func:`~grid_process.da_utils.da_to_ds` which is the inverse function
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
vars_attrs = {varname: da.attrs for varname, da in ds.data_vars.items()}
|
|
48
|
+
da = ds.to_array()
|
|
49
|
+
return da, vars_attrs
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cast_dtype(ds_out: xr.Dataset, ds_in: xr.Dataset) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Cast back each data variable in the output dataset to the original dtype
|
|
55
|
+
of the input dataset. The operation is done in-place.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
for varname, da in ds_out.items():
|
|
59
|
+
dtype_in = ds_in[varname].dtype
|
|
60
|
+
if da.dtype != dtype_in:
|
|
61
|
+
ds_out[varname] = da.astype(dtype_in)
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
|
|
2
|
+
#
|
|
3
|
+
# Distributed under the terms of the BSD 3-Clause License.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Module to compute ensemble statistics
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Literal, get_args
|
|
13
|
+
from collections.abc import Callable, Sequence
|
|
14
|
+
from functools import partial
|
|
15
|
+
from warnings import warn
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import xarray as xr
|
|
19
|
+
|
|
20
|
+
from grid_process.helpers import make_accessor, keepattrs
|
|
21
|
+
from grid_process.data_model import get_t_dim, get_spatial_dims, get_ensemble_dim
|
|
22
|
+
from grid_process.aggreg import interval_stat
|
|
23
|
+
from grid_process.da_utils import da_to_ds
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Register xarray accessor so module functions can also be called as methods
|
|
29
|
+
# on xr.Dataset objects
|
|
30
|
+
@xr.register_dataset_accessor("ens_stats")
|
|
31
|
+
class EnsStats:
|
|
32
|
+
"""
|
|
33
|
+
Xarray accessors class for extracting probabilities from ensemble models
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, ds: xr.Dataset):
|
|
37
|
+
self._data = ds
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
dataset_accessor = partial(make_accessor, accessor_class=EnsStats)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
NeighborStatMethods = Literal["mean", "sum", "min", "max"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataset_accessor
|
|
47
|
+
def agg_prob(
|
|
48
|
+
ds: xr.Dataset,
|
|
49
|
+
dict_prob: dict,
|
|
50
|
+
agg_func: Callable,
|
|
51
|
+
lt_duplicates: bool = True,
|
|
52
|
+
sign: str = "above",
|
|
53
|
+
) -> xr.Dataset:
|
|
54
|
+
"""
|
|
55
|
+
Compute aggregated probabilities of exceedance. Temporal aggregation is
|
|
56
|
+
done with :func:`~grid_process.aggreg.interval_stat`.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
ds : xarray.Dataset
|
|
61
|
+
Must have valid time and ensemble dimensions and contain a single data variable.
|
|
62
|
+
Typical dimensions names are supported, and can be extended via environment variables:
|
|
63
|
+
|
|
64
|
+
- Valid time: export GRID_PROCESS__data_model__validtime (e.g., '["time", "valid_time", "dummy_time"]')
|
|
65
|
+
- Ensemble: export GRID_PROCESS__data_model__ens_dim (e.g., '["realization", "eps", "epsd_1", "number",
|
|
66
|
+
"dummy_ens_dim"]')
|
|
67
|
+
- When the coordinates variables have a CF compliant `standard_name` attribute, arbitrary names can be
|
|
68
|
+
used directly.
|
|
69
|
+
dict_prob : dict
|
|
70
|
+
holds the keys 'prob_names', 'thresholds', 'granularity', 'interval',
|
|
71
|
+
'offset' giving the specificities for the aggregated probabilities. If
|
|
72
|
+
'prob_names' and 'thresholds' contain a list, the probabilites for
|
|
73
|
+
several thresholds but same granularity, interval and offset are
|
|
74
|
+
computed and stored in the same dataset as different variables.
|
|
75
|
+
|
|
76
|
+
- dict_prob['prob_names']: str or list of str. Name(s) given to
|
|
77
|
+
resulting data variable(s)
|
|
78
|
+
- dict_prob['thresholds']: float or list of floats. Threshold to
|
|
79
|
+
compute each probability
|
|
80
|
+
- dict_prob['granularity']: int. Granularity in hours for the
|
|
81
|
+
temporal aggregation
|
|
82
|
+
- dict_prob['interval']: int. Interval in hours for the temporal
|
|
83
|
+
aggregation
|
|
84
|
+
- dict_prob['offset']: int. Offset in hours from 00:00 for the
|
|
85
|
+
temporal aggregation
|
|
86
|
+
|
|
87
|
+
agg_func : callable
|
|
88
|
+
specify temporal aggregation function to use. The function must operate
|
|
89
|
+
on `window` dimension. E.g. ``agg_func`` = lambda x: x.mean('window')
|
|
90
|
+
lt_duplicates : bool, optional
|
|
91
|
+
if True, aggregations are done in the DWH compliant style, which means
|
|
92
|
+
intervals are calculated with respect to both UTC+1 and UTC+2. Default
|
|
93
|
+
is True
|
|
94
|
+
sign : {'above', 'below'}
|
|
95
|
+
specify if we are looking for the probability to be above or below the
|
|
96
|
+
thresholds. Default is 'above'
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
ds_out : xarray.Dataset
|
|
101
|
+
dataset with ensemble members dimension reduced and containing the
|
|
102
|
+
aggregated probabilities of exceedance for each given threshold as data
|
|
103
|
+
variables
|
|
104
|
+
|
|
105
|
+
See also
|
|
106
|
+
--------
|
|
107
|
+
:func:`~grid_process.aggreg.interval_stat`, :func:`ens_prob`
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
agg_ds = interval_stat(
|
|
111
|
+
ds,
|
|
112
|
+
dict_prob["granularity"],
|
|
113
|
+
dict_prob["interval"],
|
|
114
|
+
dict_prob["offset"],
|
|
115
|
+
agg_func,
|
|
116
|
+
lt_duplicates,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return ens_prob(agg_ds, dict_prob["thresholds"], dict_prob["prob_names"], sign)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataset_accessor
|
|
123
|
+
def ens_prob(
|
|
124
|
+
ds: xr.Dataset,
|
|
125
|
+
thresholds: float | list[float],
|
|
126
|
+
prob_names: str | list[str],
|
|
127
|
+
sign: str = "above",
|
|
128
|
+
) -> xr.Dataset:
|
|
129
|
+
"""
|
|
130
|
+
Compute probabilities of exceedance based on raw members.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
ds : xarray.Dataset
|
|
135
|
+
Must have an ensemble dimension and contain a single data variable.
|
|
136
|
+
Typical ensemble dimension names are supported, and can be extended via environment variables:
|
|
137
|
+
|
|
138
|
+
- Ensemble: export GRID_PROCESS__data_model__ens_dim (e.g., '["realization", "eps", "epsd_1", "number",
|
|
139
|
+
"dummy_ens_dim"]')
|
|
140
|
+
- When the coordinates variables have a CF compliant `standard_name` attribute, arbitrary names can be
|
|
141
|
+
used directly.
|
|
142
|
+
thresholds : float or list of floats
|
|
143
|
+
the threshold(s) to compute the exceedence probabilities. Must be in
|
|
144
|
+
the same units as the data.
|
|
145
|
+
prob_names : str or list of str
|
|
146
|
+
new variable name for each probability threshold
|
|
147
|
+
sign : {'above', 'below'}
|
|
148
|
+
specify if we are looking for the probability to be above or below the
|
|
149
|
+
thresholds. Default is 'above'
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
ds_out : xarray.Dataset
|
|
154
|
+
dataset with the ensemble members dimension reduced and containing the
|
|
155
|
+
probabilities of exceedance for each given threshold as a new data
|
|
156
|
+
variable
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
if not isinstance(thresholds, list):
|
|
160
|
+
thresholds = [thresholds]
|
|
161
|
+
if not isinstance(prob_names, list):
|
|
162
|
+
prob_names = [prob_names]
|
|
163
|
+
|
|
164
|
+
thresh_da = xr.DataArray(thresholds, dims="variable", coords={"variable": prob_names})
|
|
165
|
+
|
|
166
|
+
ens_dim = get_ensemble_dim(ds)
|
|
167
|
+
if sign == "below":
|
|
168
|
+
ds_out = (ds < thresh_da).mean(dim=ens_dim, skipna=False).astype(np.float32) * 100
|
|
169
|
+
else:
|
|
170
|
+
ds_out = (ds >= thresh_da).mean(dim=ens_dim, skipna=False).astype(np.float32) * 100
|
|
171
|
+
|
|
172
|
+
vars_attrs = {varname: {"units": "percent"} for varname in prob_names}
|
|
173
|
+
ds_out = da_to_ds(ds_out[list(ds.data_vars)[0]], vars_attrs)
|
|
174
|
+
return ds_out.assign_attrs(ds.attrs)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataset_accessor
|
|
178
|
+
def neighbor_stat(
|
|
179
|
+
ds: xr.Dataset,
|
|
180
|
+
method: NeighborStatMethods,
|
|
181
|
+
w_size_xy: int | None = None,
|
|
182
|
+
w_size_t: int | None = None,
|
|
183
|
+
) -> xr.Dataset:
|
|
184
|
+
"""
|
|
185
|
+
Compute a space x time neighborhood statistic.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
ds : xarray.Dataset
|
|
190
|
+
Must have spatial and/or temporal dimensions.
|
|
191
|
+
Typical dimensions names are supported, and can be extended via environment variables:
|
|
192
|
+
|
|
193
|
+
- Valid time: export GRID_PROCESS__data_model__validtime (e.g., '["time", "valid_time", "dummy_time"]')
|
|
194
|
+
- Lead time: export GRID_PROCESS__data_model__leadtime (e.g., '["lead_time", "step", "dummy_leadtime"]')
|
|
195
|
+
- Y coordinate: export GRID_PROCESS__data_model__grid__y (e.g., '["y", "latitude", "lat", "dummy_y"]')
|
|
196
|
+
- X coordinate: export GRID_PROCESS__data_model__grid__x (e.g., '["x", "longitude", "lon", "dummy_x"]')
|
|
197
|
+
- When the coordinates variables have a CF compliant `standard_name` attribute, arbitrary names can be
|
|
198
|
+
used directly.
|
|
199
|
+
method : {'mean', 'sum', 'min', 'max'}
|
|
200
|
+
statistic applied over the moving window/volume
|
|
201
|
+
w_size_xy : int, optional
|
|
202
|
+
window size in the spatial dimensions
|
|
203
|
+
w_size_t : int, optional
|
|
204
|
+
window size in the temporal dimension
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
ds_out : xarray.Dataset
|
|
209
|
+
each value is the result of the neighborhood statistic
|
|
210
|
+
|
|
211
|
+
Note
|
|
212
|
+
----
|
|
213
|
+
For a large gain in performence, the function performs separate rolling window
|
|
214
|
+
operations on each of the spatial and temporal dimensions sequentially. Therefore,
|
|
215
|
+
only methods for which this is equivalent than applying the statistic over the
|
|
216
|
+
rolling volume are accepted. Methods such as 'median' or 'std' are thus not accepted.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
ok_meth = get_args(NeighborStatMethods)
|
|
220
|
+
if method not in ok_meth:
|
|
221
|
+
raise ValueError(f"method must be one of {ok_meth} but was {method!r}")
|
|
222
|
+
|
|
223
|
+
if w_size_xy is not None:
|
|
224
|
+
spatial_dims = get_spatial_dims(ds)
|
|
225
|
+
if len(spatial_dims) == 1:
|
|
226
|
+
warn(
|
|
227
|
+
"Rolling window operations on unstructured grids is not supported."
|
|
228
|
+
" Resample to a 2D grid with grid_interp.interp2grid() first."
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
ds = _rolling_stat(ds, spatial_dims[1], w_size_xy, method)
|
|
232
|
+
ds = _rolling_stat(ds, spatial_dims[0], w_size_xy, method)
|
|
233
|
+
if w_size_t is not None:
|
|
234
|
+
ds = _rolling_stat(ds, get_t_dim(ds), w_size_t, method)
|
|
235
|
+
|
|
236
|
+
return ds
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _rolling_stat(ds: xr.Dataset, dim: str, w_size: int, method: str) -> xr.Dataset:
|
|
240
|
+
"""
|
|
241
|
+
Perform rolling window operation along the specified dimension.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
return getattr(ds.rolling({dim: w_size}, min_periods=1, center=True), method)(skipna=False)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@dataset_accessor
|
|
248
|
+
def upscaled_prob(
|
|
249
|
+
ds: xr.Dataset,
|
|
250
|
+
thresholds: float | list[float],
|
|
251
|
+
prob_names: str | list[str],
|
|
252
|
+
w_size_xy: int | None = None,
|
|
253
|
+
w_size_t: int | None = None,
|
|
254
|
+
) -> xr.Dataset:
|
|
255
|
+
"""
|
|
256
|
+
Compute upscaled probabilities of exceedance. Upscaling is done in the
|
|
257
|
+
space and/or time dimensions.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
ds : xarray.Dataset
|
|
262
|
+
Must have an ensemble member dimension as well as a spatial and/or time one.
|
|
263
|
+
Typical dimensions names are supported, and can be extended via environment variables:
|
|
264
|
+
|
|
265
|
+
- Ensemble: export GRID_PROCESS__data_model__ens_dim (e.g., '["realization", "eps", "epsd_1", "number",
|
|
266
|
+
"dummy_ens_dim"]')
|
|
267
|
+
- Valid time: export GRID_PROCESS__data_model__validtime (e.g., '["time", "valid_time", "dummy_time"]')
|
|
268
|
+
- Lead time: export GRID_PROCESS__data_model__leadtime (e.g., '["lead_time", "step", "dummy_leadtime"]')
|
|
269
|
+
- Y coordinate: export GRID_PROCESS__data_model__grid__y (e.g., '["y", "latitude", "lat", "dummy_y"]')
|
|
270
|
+
- X coordinate: export GRID_PROCESS__data_model__grid__x (e.g., '["x", "longitude", "lon", "dummy_x"]')
|
|
271
|
+
- When the coordinates variables have a CF compliant `standard_name` attribute, arbitrary names can be
|
|
272
|
+
used directly.
|
|
273
|
+
thresholds : float or list of floats
|
|
274
|
+
the threshold(s) to compute the exceedence probabilities. Must be in
|
|
275
|
+
the same units as the data
|
|
276
|
+
prob_names : str or list of str
|
|
277
|
+
new variable name for each probability threshold
|
|
278
|
+
w_size_xy : int, optional
|
|
279
|
+
window size for upscaling in the spatial dimensions
|
|
280
|
+
w_size_t : int, optional
|
|
281
|
+
window size for upscaling in the time dimension
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
ds_out : xarray.Dataset
|
|
286
|
+
dataset with ensemble members dimension reduced and containing the
|
|
287
|
+
upscaled probabilities of exceedance for each given threshold as a new
|
|
288
|
+
data variable
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
ds = neighbor_stat(ds, "max", w_size_xy, w_size_t)
|
|
292
|
+
return ens_prob(ds, thresholds, prob_names)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@dataset_accessor
|
|
296
|
+
def circular_mean(ds: xr.Dataset, dim: str | Sequence[str] | None = None) -> xr.Dataset:
|
|
297
|
+
"""
|
|
298
|
+
Compute the mean of circular data between 0 and 360 degrees along the
|
|
299
|
+
specified dimension(s).
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
ds : xarray.Dataset
|
|
304
|
+
dim : str or sequence of str, optional
|
|
305
|
+
dimension(s) on which to compute the circular mean. If None (default)
|
|
306
|
+
look for the ensemble members dimension
|
|
307
|
+
|
|
308
|
+
Returns
|
|
309
|
+
-------
|
|
310
|
+
ds_out : xarray.Dataset
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
if dim is None:
|
|
314
|
+
dim = get_ensemble_dim(ds)
|
|
315
|
+
|
|
316
|
+
sin_sum = np.sin(ds * np.pi / 180).sum(dim, skipna=False) # type: ignore[call-overload]
|
|
317
|
+
cos_sum = np.cos(ds * np.pi / 180).sum(dim, skipna=False) # type: ignore[call-overload]
|
|
318
|
+
res = np.arctan2(sin_sum, cos_sum) * 180 / np.pi + 360
|
|
319
|
+
return res % 360
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataset_accessor
|
|
323
|
+
@keepattrs
|
|
324
|
+
def circular_quantile(
|
|
325
|
+
ds: xr.Dataset,
|
|
326
|
+
q: float | Sequence[float] | np.ndarray,
|
|
327
|
+
method: str = "linear",
|
|
328
|
+
dim: str | None = None,
|
|
329
|
+
wrap_around: bool = True,
|
|
330
|
+
) -> xr.Dataset:
|
|
331
|
+
"""
|
|
332
|
+
Compute the qth quantile(s) of circular data (0 to 360 degrees) along the
|
|
333
|
+
specified dimension.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
ds : xarray.Dataset
|
|
338
|
+
q : float in range of [0,1] or sequence of floats or ndarray of float
|
|
339
|
+
quantile(s) to compute, which must be between 0 and 1 inclusive.
|
|
340
|
+
method : str
|
|
341
|
+
this parameter specifies the interpolation method to use when the desired
|
|
342
|
+
quantile lies between two data points. See numpy.quantile for possible
|
|
343
|
+
options. Default is 'linear'
|
|
344
|
+
dim : str, optional
|
|
345
|
+
dimension on which to compute the quantiles. If None (default) look for
|
|
346
|
+
the ensemble members dimension
|
|
347
|
+
wrap_around : bool
|
|
348
|
+
whether to wrap around the results to the 0 to 360 interval. Default is
|
|
349
|
+
True
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
ds_out : xarray.Dataset
|
|
354
|
+
a `quantile` dimension replaces the dimension being reduced
|
|
355
|
+
|
|
356
|
+
See Also
|
|
357
|
+
--------
|
|
358
|
+
numpy.quantile
|
|
359
|
+
|
|
360
|
+
Note
|
|
361
|
+
----
|
|
362
|
+
This approach was inspired by this answer on stackoverflow:
|
|
363
|
+
https://stackoverflow.com/a/46453706. The simple intuition is that the
|
|
364
|
+
opposite angle to the mean of the circular samples provides the reference
|
|
365
|
+
point for their ranking.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
if not isinstance(q, (Sequence, np.ndarray)):
|
|
369
|
+
q = [q]
|
|
370
|
+
|
|
371
|
+
if dim is None:
|
|
372
|
+
dim = get_ensemble_dim(ds)
|
|
373
|
+
|
|
374
|
+
ds_out = xr.Dataset()
|
|
375
|
+
|
|
376
|
+
# compute circular mean
|
|
377
|
+
ds_mean = circular_mean(ds, dim=dim)
|
|
378
|
+
|
|
379
|
+
for varname, da in ds.items():
|
|
380
|
+
# origin is the opposite point to the mean
|
|
381
|
+
rank_origin = (ds_mean[varname] + 180) % 360
|
|
382
|
+
# transform data
|
|
383
|
+
transformed_da = da.where(da >= rank_origin, da + 360)
|
|
384
|
+
# compute quantiles
|
|
385
|
+
new_dims = list(da.dims)
|
|
386
|
+
new_dims.remove(dim)
|
|
387
|
+
new_dims = ["quantile"] + new_dims
|
|
388
|
+
axis = da.get_axis_num(dim)
|
|
389
|
+
tmp = np.quantile(transformed_da.values, q, axis=axis, method=method).astype( # type: ignore[call-overload]
|
|
390
|
+
da.dtype.type, copy=False
|
|
391
|
+
)
|
|
392
|
+
if wrap_around:
|
|
393
|
+
tmp = tmp % 360
|
|
394
|
+
ds_out[varname] = xr.DataArray(tmp, dims=new_dims, coords={"quantile": q})
|
|
395
|
+
|
|
396
|
+
return ds_out.assign_coords(ds_mean.coords)
|