obsarray 1.0.2__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.
- docs/conf.py +181 -0
- notebook/test_register_err_corr_form.py +45 -0
- obsarray/__init__.py +14 -0
- obsarray/_version.py +21 -0
- obsarray/err_corr.py +298 -0
- obsarray/flag_accessor.py +442 -0
- obsarray/templater/__init__.py +0 -0
- obsarray/templater/dataset_util.py +659 -0
- obsarray/templater/dstemplater.py +133 -0
- obsarray/templater/dswriter.py +113 -0
- obsarray/templater/template_util.py +257 -0
- obsarray/templater/tests/__init__.py +0 -0
- obsarray/templater/tests/test_dataset_util.py +488 -0
- obsarray/templater/tests/test_dswriter.py +31 -0
- obsarray/templater/tests/test_template_util.py +334 -0
- obsarray/test/__init__.py +0 -0
- obsarray/test/test_err_corr_forms.py +251 -0
- obsarray/test/test_flag_accessor.py +373 -0
- obsarray/test/test_unc_accessor.py +745 -0
- obsarray/unc_accessor.py +899 -0
- obsarray/utils.py +31 -0
- obsarray-1.0.2.dist-info/METADATA +57 -0
- obsarray-1.0.2.dist-info/RECORD +26 -0
- obsarray-1.0.2.dist-info/WHEEL +5 -0
- obsarray-1.0.2.dist-info/licenses/LICENSE +165 -0
- obsarray-1.0.2.dist-info/top_level.txt +3 -0
obsarray/unc_accessor.py
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
"""unc_accessor - xarray extensions with accessor objects for uncertainty handling"""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
import numpy as np
|
|
5
|
+
import xarray as xr
|
|
6
|
+
from typing import Union, Tuple, List, Optional
|
|
7
|
+
from comet_maths import convert_corr_to_cov, correlation_from_covariance
|
|
8
|
+
from obsarray.templater.template_util import create_var
|
|
9
|
+
from obsarray.templater.dataset_util import DatasetUtil
|
|
10
|
+
from obsarray.err_corr import err_corr_forms, BaseErrCorrForm
|
|
11
|
+
from obsarray.utils import empty_err_corr_matrix
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__author__ = "Sam Hunt <sam.hunt@npl.co.uk>"
|
|
15
|
+
__all__ = []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Uncertainty:
|
|
19
|
+
"""
|
|
20
|
+
Interface for handling ``xarray.Dataset`` uncertainty variables
|
|
21
|
+
|
|
22
|
+
:param xarray_obj: dataset
|
|
23
|
+
:param unc_var_name: name of uncertainty variable
|
|
24
|
+
:param sli: slice of variable
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
xarray_obj: xr.Dataset,
|
|
30
|
+
var_name: str,
|
|
31
|
+
unc_var_name: str,
|
|
32
|
+
sli: Optional[tuple] = None,
|
|
33
|
+
):
|
|
34
|
+
|
|
35
|
+
# initialise attributes
|
|
36
|
+
|
|
37
|
+
self._obj = xarray_obj
|
|
38
|
+
self._unc_var_name = unc_var_name
|
|
39
|
+
self._var_name = var_name
|
|
40
|
+
self._sli = tuple([slice(None)] * self._obj[self._unc_var_name].ndim)
|
|
41
|
+
if sli is not None:
|
|
42
|
+
self._sli = self._expand_sli(sli)
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
"""Custom __str__"""
|
|
46
|
+
return "<{}> \n{}".format(
|
|
47
|
+
self.__class__.__name__,
|
|
48
|
+
self.value.__repr__()[18:],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
"""Custom __repr__"""
|
|
53
|
+
return str(self)
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, sli: tuple):
|
|
56
|
+
"""
|
|
57
|
+
Defines variable slice
|
|
58
|
+
|
|
59
|
+
:param sli: slice of variable
|
|
60
|
+
:return: self
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# update slice
|
|
64
|
+
self._sli = self._expand_sli(sli)
|
|
65
|
+
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def _expand_sli(self, sli: Optional[tuple] = None) -> tuple:
|
|
69
|
+
"""
|
|
70
|
+
Function to expand the provided sli so that it always has the right number of dimensions
|
|
71
|
+
|
|
72
|
+
:param sli: input sli tuple. This one can have fewer dimensions than the total if e.g. only providing the first index.
|
|
73
|
+
:type sli: tuple
|
|
74
|
+
:return: output sli tuple.
|
|
75
|
+
:rtype: tuple
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# if no slice provided, define as slice for full array
|
|
79
|
+
if sli is None:
|
|
80
|
+
out_sli = tuple([slice(None)] * self._obj[self._unc_var_name].ndim)
|
|
81
|
+
|
|
82
|
+
# Otherwise, set each : dimension to slice(None)
|
|
83
|
+
# E.g. if providing [0] for a variable with 3 dimensions, this becomes
|
|
84
|
+
# [0,slice(None),slice(None)]
|
|
85
|
+
else:
|
|
86
|
+
out_sli = list([slice(None)] * self._obj[self._unc_var_name].ndim)
|
|
87
|
+
sli_list = list(sli) if isinstance(sli, tuple) else [sli]
|
|
88
|
+
for i in range(len(sli_list)):
|
|
89
|
+
if not sli_list[i] == ":":
|
|
90
|
+
out_sli[i] = sli_list[i]
|
|
91
|
+
out_sli = tuple(out_sli)
|
|
92
|
+
|
|
93
|
+
return out_sli
|
|
94
|
+
|
|
95
|
+
def rename(self, name) -> xr.Dataset:
|
|
96
|
+
"""
|
|
97
|
+
Renames uncertainty variable and safely updates variable name references in obs variable metadata
|
|
98
|
+
:param name: desired name
|
|
99
|
+
:returns: dataset with safely renamed variable
|
|
100
|
+
"""
|
|
101
|
+
self._obj = self._obj.rename({self._unc_var_name: name})
|
|
102
|
+
self._obj[self._var_name].attrs["unc_comps"] = [
|
|
103
|
+
v.replace(self._unc_var_name, name)
|
|
104
|
+
for v in self._obj[self._var_name].attrs["unc_comps"]
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
return self._obj
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def err_corr(self) -> List[Tuple[Union[str, List[str]], BaseErrCorrForm]]:
|
|
111
|
+
"""
|
|
112
|
+
Error-correlation parameterisation for uncertainty effect.
|
|
113
|
+
|
|
114
|
+
Given as a list of parameterisations along different variable slice dimensions. Each parameterisation is given as a two-element tuple - where the first element is the dimension (or list of dimensions) along which the parameterisation applies, and the second element is the error-correlation parameterisation defining object (as subclass of ``obsarray.err_corr.BaseErrorCorrForm``).
|
|
115
|
+
|
|
116
|
+
:return: Error correlation parameterisation
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# Find dimensions in variable slice
|
|
120
|
+
sli_dims = [
|
|
121
|
+
dim
|
|
122
|
+
for dim, idx in zip(
|
|
123
|
+
self._obj.dims.keys(), self._sli
|
|
124
|
+
) # Due to hit a FutureDeprecation warning
|
|
125
|
+
if not isinstance(idx, int)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# Find number of error-correlation parameterisations along different dimensions in variable metadata
|
|
129
|
+
err_corr_idxs = set(
|
|
130
|
+
[
|
|
131
|
+
a[9]
|
|
132
|
+
for a in self._obj[self._unc_var_name].attrs.keys()
|
|
133
|
+
if a[:8] == "err_corr"
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Loop through error-correlation parameterisations, extract metadata and build parameterisation
|
|
138
|
+
# as a two element tuple (as described in doc string)
|
|
139
|
+
err_corr = []
|
|
140
|
+
for i in err_corr_idxs:
|
|
141
|
+
|
|
142
|
+
dim_i = self._obj[self._unc_var_name].attrs[
|
|
143
|
+
DatasetUtil.return_err_corr_dim_str(i)
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
list_dim_i = deepcopy(dim_i)
|
|
147
|
+
if isinstance(dim_i, str):
|
|
148
|
+
list_dim_i = [dim_i]
|
|
149
|
+
|
|
150
|
+
# continue if parmeterisation dimension in slice dimensions
|
|
151
|
+
if [dim_i_j for dim_i_j in list_dim_i if dim_i_j in sli_dims]:
|
|
152
|
+
|
|
153
|
+
form_i = self._obj[self._unc_var_name].attrs[
|
|
154
|
+
DatasetUtil.return_err_corr_form_str(i)
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
params_i = self._obj[self._unc_var_name].attrs[
|
|
158
|
+
DatasetUtil.return_err_corr_params_str(i)
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
units_i = self._obj[self._unc_var_name].attrs[
|
|
162
|
+
DatasetUtil.return_err_corr_units_str(i)
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
err_corr.append(
|
|
166
|
+
(
|
|
167
|
+
dim_i,
|
|
168
|
+
err_corr_forms[form_i](
|
|
169
|
+
self._obj, self._unc_var_name, dim_i, params_i, units_i
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return err_corr
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def units(self) -> Union[str, None]:
|
|
178
|
+
"""
|
|
179
|
+
Return uncertainty variable units
|
|
180
|
+
|
|
181
|
+
:return: uncertainty variable units
|
|
182
|
+
"""
|
|
183
|
+
return (
|
|
184
|
+
self._obj[self._unc_var_name].attrs["units"]
|
|
185
|
+
if "units" in self._obj[self._unc_var_name].attrs
|
|
186
|
+
else None
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def var_units(self) -> Union[str, None]:
|
|
191
|
+
"""
|
|
192
|
+
Returns units of observation variable associated with uncertainty variable
|
|
193
|
+
|
|
194
|
+
:return: observation variable units
|
|
195
|
+
"""
|
|
196
|
+
return (
|
|
197
|
+
self._obj[self._var_name].attrs["units"]
|
|
198
|
+
if "units" in self._obj[self._var_name].attrs
|
|
199
|
+
else None
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def var_value(self) -> xr.DataArray:
|
|
204
|
+
"""
|
|
205
|
+
Returns value of observation variable associated with uncertainty variable
|
|
206
|
+
|
|
207
|
+
:return: observation variable
|
|
208
|
+
"""
|
|
209
|
+
return self._obj[self._var_name][self._sli]
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def value(self) -> xr.DataArray:
|
|
213
|
+
"""
|
|
214
|
+
Return uncertainty data array
|
|
215
|
+
|
|
216
|
+
:return: uncertainty variable
|
|
217
|
+
"""
|
|
218
|
+
return self._obj[self._unc_var_name][self._sli]
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def abs_value(self) -> xr.DataArray:
|
|
222
|
+
"""
|
|
223
|
+
Returns uncertainty values with units that match those of the observation variable
|
|
224
|
+
|
|
225
|
+
NB: only currently converts % to absolute, can't handle mismatched units
|
|
226
|
+
|
|
227
|
+
:return: uncertainty values with same units as observation variable
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
if self.var_units is not None:
|
|
231
|
+
if self.units == "%":
|
|
232
|
+
return self.value / 100 * self.var_value
|
|
233
|
+
|
|
234
|
+
elif (self.units != self.var_units) and (self.units is not None):
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"Unit mismatch between observation variable and uncertainty variable:\n"
|
|
237
|
+
"* '{}' - '{}'\n"
|
|
238
|
+
"* '{}' - '{}'".format(
|
|
239
|
+
self._var_name, self.var_units, self._unc_var_name, self.units
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return self.value
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def pdf_shape(self):
|
|
247
|
+
"""
|
|
248
|
+
Returns probability density function shape for uncertainty variable data
|
|
249
|
+
|
|
250
|
+
:return: uncertainty variable pdf shape
|
|
251
|
+
"""
|
|
252
|
+
return self._obj[self._unc_var_name].attrs["pdf_shape"]
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def is_random(self) -> bool:
|
|
256
|
+
"""
|
|
257
|
+
Returns True if uncertainty is fully random in all dimensions
|
|
258
|
+
|
|
259
|
+
:return: random uncertainty flag
|
|
260
|
+
"""
|
|
261
|
+
if len(self.err_corr) > 0:
|
|
262
|
+
return all(e[1].is_random is True for e in self.err_corr)
|
|
263
|
+
else:
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def is_structured(self) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Returns True if uncertainty is neither fully random or systematic in all dimensions
|
|
270
|
+
|
|
271
|
+
:return: structured uncertainty flag
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
if all(e[1].is_random is True for e in self.err_corr) or all(
|
|
275
|
+
e[1].is_systematic is True for e in self.err_corr
|
|
276
|
+
):
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def is_systematic(self) -> bool:
|
|
283
|
+
"""
|
|
284
|
+
Returns True if uncertainty is fully systematic in all dimensions
|
|
285
|
+
|
|
286
|
+
:return: systematic uncertainty flag
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
return all(e[1].is_systematic is True for e in self.err_corr)
|
|
290
|
+
|
|
291
|
+
def err_corr_dict(self) -> dict:
|
|
292
|
+
"""
|
|
293
|
+
Error-correlation dictionary for uncertainty effect.
|
|
294
|
+
|
|
295
|
+
:return: dictionary with error-correlation for each dimension
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
# initialise error-correlation dictionary
|
|
299
|
+
err_corr_dict = {}
|
|
300
|
+
|
|
301
|
+
# populate with error-correlation matrices built be each error-correlation
|
|
302
|
+
# parameterisation object
|
|
303
|
+
for dim_err_corr in self.err_corr:
|
|
304
|
+
if np.all(
|
|
305
|
+
[
|
|
306
|
+
dim in self._obj[self._unc_var_name][self._sli].dims
|
|
307
|
+
for dim in dim_err_corr[1].dims
|
|
308
|
+
]
|
|
309
|
+
):
|
|
310
|
+
if dim_err_corr[1].form in ["random", "systematic"]:
|
|
311
|
+
err_corr_dict[dim_err_corr[0]] = dim_err_corr[1].form
|
|
312
|
+
|
|
313
|
+
elif dim_err_corr[1].form == "err_corr_matrix":
|
|
314
|
+
err_corr_dict[dim_err_corr[0]] = self._obj[
|
|
315
|
+
dim_err_corr[1].params[0]
|
|
316
|
+
].values
|
|
317
|
+
|
|
318
|
+
else:
|
|
319
|
+
raise NotImplementedError(
|
|
320
|
+
"this correlation form is not implemented for err_corr_dict()"
|
|
321
|
+
)
|
|
322
|
+
return err_corr_dict
|
|
323
|
+
|
|
324
|
+
def err_corr_matrix(self) -> xr.DataArray:
|
|
325
|
+
"""
|
|
326
|
+
Error-correlation matrix for uncertainty effect.
|
|
327
|
+
|
|
328
|
+
:return: Error-correlation matrix
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
# initialise error-correlation matrix
|
|
332
|
+
err_corr_matrix = empty_err_corr_matrix(
|
|
333
|
+
self._obj[self._unc_var_name][self._sli]
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# populate with error-correlation matrices built be each error-correlation
|
|
337
|
+
# parameterisation object
|
|
338
|
+
for dim_err_corr in self.err_corr:
|
|
339
|
+
sliced_dims = dim_err_corr[1].get_sliced_dims_errcorr(self._sli)
|
|
340
|
+
if len(dim_err_corr[1].get_sliced_dims_errcorr(self._sli)) > 0:
|
|
341
|
+
err_corr_matrix.values = err_corr_matrix.values.dot(
|
|
342
|
+
dim_err_corr[1].build_dot_matrix(self._sli)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return err_corr_matrix
|
|
346
|
+
|
|
347
|
+
def err_cov_matrix(self):
|
|
348
|
+
"""
|
|
349
|
+
Error-covariance matrix for uncertainty effect
|
|
350
|
+
|
|
351
|
+
:return: Error-covariance matrix
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
err_cov_matrix = empty_err_corr_matrix(self._obj[self._unc_var_name][self._sli])
|
|
355
|
+
|
|
356
|
+
err_cov_matrix.values = convert_corr_to_cov(
|
|
357
|
+
self.err_corr_matrix().values, self.abs_value.values
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return err_cov_matrix
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class VariableUncertainty:
|
|
364
|
+
"""
|
|
365
|
+
Interface for ``xarray.Dataset`` variable uncertainty handling
|
|
366
|
+
|
|
367
|
+
:param xarray_obj: dataset
|
|
368
|
+
:param var_name: name of dataset variable
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def __init__(self, xarray_obj: xr.Dataset, var_name: str):
|
|
372
|
+
self._obj = xarray_obj
|
|
373
|
+
self._var_name = var_name
|
|
374
|
+
self._sli = tuple([slice(None)] * self._obj[self._var_name].ndim)
|
|
375
|
+
|
|
376
|
+
def __getitem__(
|
|
377
|
+
self, key: Union[str, tuple]
|
|
378
|
+
) -> Union[Uncertainty, "VariableUncertainty"]:
|
|
379
|
+
"""
|
|
380
|
+
Returns variable uncertainty interface
|
|
381
|
+
|
|
382
|
+
:param key: uncertainty variable name or variable slice
|
|
383
|
+
:return: uncertainty interface
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
if isinstance(key, str):
|
|
387
|
+
return Uncertainty(self._obj, self._var_name, key, self._sli)
|
|
388
|
+
|
|
389
|
+
self._sli = key
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def __setitem__(
|
|
393
|
+
self,
|
|
394
|
+
unc_var: str,
|
|
395
|
+
unc_def: Union[
|
|
396
|
+
xr.DataArray, xr.Variable, Tuple[List[str], np.ndarray, Optional[dict]]
|
|
397
|
+
],
|
|
398
|
+
):
|
|
399
|
+
"""
|
|
400
|
+
Adds defined uncertainty variable to dataset
|
|
401
|
+
|
|
402
|
+
:param unc_var: uncertainty variable name
|
|
403
|
+
:param unc_def: either xarray DataArray/Variable, or definition through tuple as ``(dims, data[, attrs])``. ``dims`` is a list of variable dimension names, ``data`` is a numpy array of uncertainty values and ``attrs`` is a dictionary of variable attributes. ``attrs`` should include an element ``pdf_shape`` which defines the uncertainty probability density function form, and ``err_corr`` which defines the error-correlation structure of the data. If omitted ``pdf_shape`` is assumed Gaussian and the error-correlation is assumed random.
|
|
404
|
+
|
|
405
|
+
:return: uncertainty variable interface
|
|
406
|
+
"""
|
|
407
|
+
|
|
408
|
+
self._obj.unc._add_unc_var(self._var_name, unc_var, unc_def)
|
|
409
|
+
|
|
410
|
+
def __delitem__(self, unc_var):
|
|
411
|
+
"""
|
|
412
|
+
Safely removes uncertainty variable
|
|
413
|
+
|
|
414
|
+
:param unc_var: uncertainty variable name
|
|
415
|
+
"""
|
|
416
|
+
self._obj.unc._remove_unc_var(self._var_name, unc_var)
|
|
417
|
+
|
|
418
|
+
def __str__(self):
|
|
419
|
+
"""Custom __str__"""
|
|
420
|
+
return "<{}>\nVariable Uncertainties: '{}'\n{}".format(
|
|
421
|
+
self.__class__.__name__,
|
|
422
|
+
self._var_name,
|
|
423
|
+
self._obj.unc._var_unc_vars(self._var_name).__repr__(),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def __repr__(self):
|
|
427
|
+
"""Custom __repr__"""
|
|
428
|
+
return str(self)
|
|
429
|
+
|
|
430
|
+
def __len__(self) -> int:
|
|
431
|
+
"""
|
|
432
|
+
Returns number of variable uncertainties
|
|
433
|
+
|
|
434
|
+
:returns: number of variable uncertainties
|
|
435
|
+
"""
|
|
436
|
+
return len(self._obj.unc._var_unc_var_names(self._var_name))
|
|
437
|
+
|
|
438
|
+
def __iter__(self):
|
|
439
|
+
"""Custom __iter__"""
|
|
440
|
+
|
|
441
|
+
self.i = 0 # Define counter
|
|
442
|
+
return self
|
|
443
|
+
|
|
444
|
+
def __next__(self) -> Uncertainty:
|
|
445
|
+
"""
|
|
446
|
+
Returns ith variable uncertainty
|
|
447
|
+
|
|
448
|
+
:return: uncertainty variable
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
# Iterate through uncertainty comp
|
|
452
|
+
if self.i < len(self.keys()):
|
|
453
|
+
self.i += 1 # Update counter
|
|
454
|
+
return self[self.keys()[self.i - 1]]
|
|
455
|
+
|
|
456
|
+
else:
|
|
457
|
+
raise StopIteration
|
|
458
|
+
|
|
459
|
+
def keys(self) -> List[str]:
|
|
460
|
+
"""
|
|
461
|
+
Returns uncertainty variable names
|
|
462
|
+
|
|
463
|
+
:return: uncertainty variable names
|
|
464
|
+
"""
|
|
465
|
+
return self._obj.unc._var_unc_var_names(self._var_name)
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def comps(self) -> xr.core.dataset.DataVariables:
|
|
469
|
+
"""
|
|
470
|
+
Returns observation variable uncertainty data variables
|
|
471
|
+
|
|
472
|
+
:return: uncertainty data variables
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
return self._obj.unc._var_unc_vars(self._var_name, self._sli)
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def random_comps(self) -> xr.core.dataset.DataVariables:
|
|
479
|
+
"""
|
|
480
|
+
Returns dataset uncertainty data variables with fully random error-correlation
|
|
481
|
+
|
|
482
|
+
:return: uncertainty data variables
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
return self._obj.unc._var_unc_vars(self._var_name, self._sli, unc_type="random")
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def structured_comps(self) -> xr.core.dataset.DataVariables:
|
|
489
|
+
"""
|
|
490
|
+
Returns dataset uncertainty data variables with structured error-correlation
|
|
491
|
+
|
|
492
|
+
:return: uncertainty data variables
|
|
493
|
+
"""
|
|
494
|
+
|
|
495
|
+
return self._obj.unc._var_unc_vars(
|
|
496
|
+
self._var_name, self._sli, unc_type="structured"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def systematic_comps(self) -> xr.core.dataset.DataVariables:
|
|
501
|
+
"""
|
|
502
|
+
Returns dataset uncertainty data variables with fully systematic error-correlation
|
|
503
|
+
|
|
504
|
+
:return: uncertainty data variables
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
return self._obj.unc._var_unc_vars(
|
|
508
|
+
self._var_name, self._sli, unc_type="systematic"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
def _quadsum_unc(self, unc_var_names):
|
|
512
|
+
quadsum_unc = None
|
|
513
|
+
for i, unc_var_name in enumerate(unc_var_names):
|
|
514
|
+
if i == 0:
|
|
515
|
+
quadsum_unc = deepcopy(self[unc_var_name].abs_value ** 2.0)
|
|
516
|
+
quadsum_unc.name = None
|
|
517
|
+
else:
|
|
518
|
+
quadsum_unc += self[unc_var_name].abs_value ** 2.0
|
|
519
|
+
|
|
520
|
+
if quadsum_unc is not None:
|
|
521
|
+
quadsum_unc = quadsum_unc**0.5
|
|
522
|
+
|
|
523
|
+
return quadsum_unc
|
|
524
|
+
|
|
525
|
+
def total_unc(self) -> xr.DataArray:
|
|
526
|
+
"""
|
|
527
|
+
Returns observation variable combined uncertainty for all uncertainty components (in absolute units)
|
|
528
|
+
|
|
529
|
+
:return: total observation variable uncertainty
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
return self._quadsum_unc(self._obj.unc._var_unc_var_names(self._var_name))
|
|
533
|
+
|
|
534
|
+
def random_unc(self) -> xr.DataArray:
|
|
535
|
+
"""
|
|
536
|
+
Returns observation variable combined uncertainty for uncertainty components with fully random error-correlation
|
|
537
|
+
|
|
538
|
+
:return: total random observation variable uncertainty
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
return self._quadsum_unc(
|
|
542
|
+
self._obj.unc._var_unc_var_names(self._var_name, unc_type="random")
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def structured_unc(self) -> xr.DataArray:
|
|
546
|
+
"""
|
|
547
|
+
Returns observation variable combined uncertainty for uncertainty components with structured error-correlation
|
|
548
|
+
|
|
549
|
+
:return: total structured observation variable uncertainty
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
return self._quadsum_unc(
|
|
553
|
+
self._obj.unc._var_unc_var_names(self._var_name, unc_type="structured")
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
def systematic_unc(self) -> xr.DataArray:
|
|
557
|
+
"""
|
|
558
|
+
Returns observation variable combined uncertainty for uncertainty components with fully systematic error-correlation
|
|
559
|
+
|
|
560
|
+
:return: total systematic observation variable uncertainty
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
return self._quadsum_unc(
|
|
564
|
+
self._obj.unc._var_unc_var_names(self._var_name, unc_type="systematic")
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def total_err_corr_matrix(self) -> xr.DataArray:
|
|
568
|
+
"""
|
|
569
|
+
Returns observation variable combined error-correlation matrix for all uncertainty components
|
|
570
|
+
|
|
571
|
+
:return: total error-correlation matrix
|
|
572
|
+
"""
|
|
573
|
+
|
|
574
|
+
total_err_corr_matrix = empty_err_corr_matrix(
|
|
575
|
+
self._obj[self._var_name][self._sli]
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
total_err_corr_matrix.values = correlation_from_covariance(
|
|
579
|
+
self.total_err_cov_matrix().values
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
return total_err_corr_matrix
|
|
583
|
+
|
|
584
|
+
def structured_err_corr_matrix(self) -> xr.DataArray:
|
|
585
|
+
"""
|
|
586
|
+
Returns observation variable combined error-correlation matrix for uncertainty components that do not have either fully random or fully systematic error-correlation
|
|
587
|
+
|
|
588
|
+
:return: structured error-correlation matrix
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
structured_err_corr_matrix = empty_err_corr_matrix(
|
|
592
|
+
self._obj[self._var_name][self._sli]
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
structured_err_corr_matrix.values = correlation_from_covariance(
|
|
596
|
+
self.structured_err_cov_matrix().values
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
return structured_err_corr_matrix
|
|
600
|
+
|
|
601
|
+
def total_err_cov_matrix(self) -> xr.DataArray:
|
|
602
|
+
"""
|
|
603
|
+
Returns observation variable combined error-covariance matrix for all uncertainty components
|
|
604
|
+
|
|
605
|
+
:return: total error-covariance matrix
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
total_err_cov_matrix = empty_err_corr_matrix(
|
|
609
|
+
self._obj[self._var_name][self._sli]
|
|
610
|
+
)
|
|
611
|
+
covs_sum = np.zeros(total_err_cov_matrix.shape)
|
|
612
|
+
for unc in self:
|
|
613
|
+
covs_sum += unc[self._sli].err_cov_matrix().values
|
|
614
|
+
|
|
615
|
+
total_err_cov_matrix.values = covs_sum
|
|
616
|
+
|
|
617
|
+
return total_err_cov_matrix
|
|
618
|
+
|
|
619
|
+
def structured_err_cov_matrix(self):
|
|
620
|
+
"""
|
|
621
|
+
Returns observation variable combined error-covariance matrix for uncertainty components with structured error-correlation
|
|
622
|
+
|
|
623
|
+
:return: structured error-covariance matrix
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
structured_err_cov_matrix = empty_err_corr_matrix(
|
|
627
|
+
self._obj[self._var_name][self._sli]
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
covs_sum = np.zeros(structured_err_cov_matrix.shape)
|
|
631
|
+
for unc in self:
|
|
632
|
+
if unc.is_structured:
|
|
633
|
+
covs_sum += unc[self._sli].err_cov_matrix().values
|
|
634
|
+
|
|
635
|
+
structured_err_cov_matrix.values = covs_sum
|
|
636
|
+
|
|
637
|
+
return structured_err_cov_matrix
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
@xr.register_dataset_accessor("unc")
|
|
641
|
+
class UncAccessor(object):
|
|
642
|
+
"""
|
|
643
|
+
``xarray.Dataset`` accesssor object for handling dataset variable uncertainties
|
|
644
|
+
|
|
645
|
+
:param xarray_obj: xarray dataset
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
def __init__(self, xarray_obj: xr.Dataset):
|
|
649
|
+
|
|
650
|
+
# Initialise attributes
|
|
651
|
+
self._obj = xarray_obj
|
|
652
|
+
|
|
653
|
+
def __str__(self) -> str:
|
|
654
|
+
"""Custom __str__"""
|
|
655
|
+
|
|
656
|
+
string = "<{}>\nDataset Uncertainties:\n".format(self.__class__.__name__)
|
|
657
|
+
|
|
658
|
+
for var_name in self.keys():
|
|
659
|
+
string += "* {}\n{}\n".format(
|
|
660
|
+
var_name, self._obj.unc._var_unc_vars(var_name).__repr__()[16:]
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
return string
|
|
664
|
+
|
|
665
|
+
def __repr__(self) -> str:
|
|
666
|
+
"""Custom __repr__"""
|
|
667
|
+
return str(self)
|
|
668
|
+
|
|
669
|
+
def __getitem__(self, var_name: str) -> VariableUncertainty:
|
|
670
|
+
"""
|
|
671
|
+
Returns :py:class:`~obsarray.unc_accessor.VariableUncertainty` object to interface with uncertainty information for a given observation variable
|
|
672
|
+
|
|
673
|
+
:param var_name: observation variable name
|
|
674
|
+
:return: observation variable uncertainty interface
|
|
675
|
+
"""
|
|
676
|
+
return VariableUncertainty(self._obj, var_name)
|
|
677
|
+
|
|
678
|
+
def __len__(self):
|
|
679
|
+
"""
|
|
680
|
+
Returns number of observation variables
|
|
681
|
+
|
|
682
|
+
:return: number of observation variables
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
return len(self.keys())
|
|
686
|
+
|
|
687
|
+
def __iter__(self) -> "UncAccessor":
|
|
688
|
+
"""
|
|
689
|
+
Initialises iterator
|
|
690
|
+
|
|
691
|
+
:return: self
|
|
692
|
+
"""
|
|
693
|
+
self.i = 0 # Define counter
|
|
694
|
+
return self
|
|
695
|
+
|
|
696
|
+
def __next__(self):
|
|
697
|
+
"""
|
|
698
|
+
Returns ith observation variable
|
|
699
|
+
|
|
700
|
+
:return: ith obs variable
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
# Iterate through obs variables
|
|
704
|
+
if self.i < len(self.keys()):
|
|
705
|
+
self.i += 1 # Update counter
|
|
706
|
+
return self[self.keys()[self.i - 1]]
|
|
707
|
+
|
|
708
|
+
else:
|
|
709
|
+
raise StopIteration
|
|
710
|
+
|
|
711
|
+
def keys(self) -> list:
|
|
712
|
+
"""
|
|
713
|
+
Returns observation variable names
|
|
714
|
+
|
|
715
|
+
:return: observation variable names
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
return list(self._obj.unc.obs_vars.keys())
|
|
719
|
+
|
|
720
|
+
@property
|
|
721
|
+
def obs_vars(self) -> xr.core.dataset.DataVariables:
|
|
722
|
+
"""
|
|
723
|
+
Returns dataset observation data variables (defined as dataset variables with uncertainties)
|
|
724
|
+
|
|
725
|
+
:return: observation data variables
|
|
726
|
+
"""
|
|
727
|
+
obs_var_names = []
|
|
728
|
+
for var_name in self._obj.variables:
|
|
729
|
+
if self._is_obs_var(var_name):
|
|
730
|
+
obs_var_names.append(var_name)
|
|
731
|
+
return self._obj[obs_var_names].data_vars
|
|
732
|
+
|
|
733
|
+
@property
|
|
734
|
+
def unc_vars(self) -> xr.core.dataset.DataVariables:
|
|
735
|
+
"""
|
|
736
|
+
Returns dataset uncertainty data variables (defined as uncertainties associated with observation variables)
|
|
737
|
+
|
|
738
|
+
:return: uncertainty data variables
|
|
739
|
+
"""
|
|
740
|
+
unc_var_names = set()
|
|
741
|
+
for var_name in self.obs_vars:
|
|
742
|
+
var_unc_var_names = self._var_unc_var_names(var_name)
|
|
743
|
+
|
|
744
|
+
if type(var_unc_var_names) == str:
|
|
745
|
+
var_unc_var_names = [var_unc_var_names]
|
|
746
|
+
|
|
747
|
+
unc_var_names |= set(var_unc_var_names)
|
|
748
|
+
return self._obj[list(unc_var_names)].data_vars
|
|
749
|
+
|
|
750
|
+
def _var_unc_var_names(
|
|
751
|
+
self, obs_var_name: str, unc_type: Optional[str] = None
|
|
752
|
+
) -> List[str]:
|
|
753
|
+
"""
|
|
754
|
+
Returns the names of uncertainty variables associated with specified observation variable
|
|
755
|
+
|
|
756
|
+
:param obs_var_name: observation variable name
|
|
757
|
+
:param unc_type: option to filter for specific uncertainty types, must have value ``"random"``, ``"structured"`` or ``"systematic"``
|
|
758
|
+
|
|
759
|
+
:return: uncertainty variable names
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
# Get the names of unc vars defined in obs var metadata
|
|
763
|
+
all_unc_var_names = []
|
|
764
|
+
if "unc_comps" in self._obj[obs_var_name].attrs:
|
|
765
|
+
all_unc_var_names = self._obj[obs_var_name].attrs["unc_comps"]
|
|
766
|
+
if type(all_unc_var_names) == str:
|
|
767
|
+
all_unc_var_names = [all_unc_var_names]
|
|
768
|
+
|
|
769
|
+
# Filter returned names if unc_type defined
|
|
770
|
+
unc_var_names = []
|
|
771
|
+
if unc_type is None:
|
|
772
|
+
unc_var_names = all_unc_var_names
|
|
773
|
+
|
|
774
|
+
elif (
|
|
775
|
+
(unc_type == "random")
|
|
776
|
+
or (unc_type == "structured")
|
|
777
|
+
or (unc_type == "systematic")
|
|
778
|
+
):
|
|
779
|
+
for unc_var in all_unc_var_names:
|
|
780
|
+
if getattr(self[obs_var_name][unc_var], "is_" + unc_type):
|
|
781
|
+
unc_var_names.append(unc_var)
|
|
782
|
+
|
|
783
|
+
else:
|
|
784
|
+
raise ValueError("no unc_type " + unc_type)
|
|
785
|
+
|
|
786
|
+
return unc_var_names
|
|
787
|
+
|
|
788
|
+
def _is_obs_var(self, var_name: str) -> bool:
|
|
789
|
+
"""
|
|
790
|
+
Returns true if named dataset variable is an observation variable
|
|
791
|
+
|
|
792
|
+
:return: observation variable flag
|
|
793
|
+
"""
|
|
794
|
+
|
|
795
|
+
if self._var_unc_var_names(var_name):
|
|
796
|
+
return True
|
|
797
|
+
return False
|
|
798
|
+
|
|
799
|
+
def _is_unc_var(self, var_name: str) -> bool:
|
|
800
|
+
"""
|
|
801
|
+
Returns true if named dataset variable is an uncertainty variable
|
|
802
|
+
|
|
803
|
+
:return: uncertainty variable flag
|
|
804
|
+
"""
|
|
805
|
+
|
|
806
|
+
if var_name in self.unc_vars:
|
|
807
|
+
return True
|
|
808
|
+
return False
|
|
809
|
+
|
|
810
|
+
def _var_unc_vars(
|
|
811
|
+
self,
|
|
812
|
+
obs_var_name: str,
|
|
813
|
+
sli: Optional[tuple] = None,
|
|
814
|
+
unc_type: Optional[str] = None,
|
|
815
|
+
) -> xr.core.dataset.DataVariables:
|
|
816
|
+
"""
|
|
817
|
+
Returns uncertainty data variables for specified observation variable
|
|
818
|
+
|
|
819
|
+
:return: uncertainty data variables
|
|
820
|
+
"""
|
|
821
|
+
|
|
822
|
+
unc_var_names = self._var_unc_var_names(obs_var_name, unc_type=unc_type)
|
|
823
|
+
if len(unc_var_names) == 0:
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
if sli is None:
|
|
827
|
+
return self._obj[unc_var_names].data_vars
|
|
828
|
+
|
|
829
|
+
isel_sli = {dim: s for dim, s in zip(self._obj[unc_var_names[0]].dims, sli)}
|
|
830
|
+
return self._obj[unc_var_names].isel(isel_sli).data_vars
|
|
831
|
+
|
|
832
|
+
def _add_unc_var(
|
|
833
|
+
self,
|
|
834
|
+
obs_var: str,
|
|
835
|
+
unc_var: str,
|
|
836
|
+
unc_def: Union[xr.DataArray, Tuple[List[str], np.ndarray, Optional[dict]]],
|
|
837
|
+
) -> None:
|
|
838
|
+
"""
|
|
839
|
+
Adds an uncertainty variable to the dataset
|
|
840
|
+
|
|
841
|
+
:param obs_var: associated observation variable name
|
|
842
|
+
:param unc_var: uncertainty variable name
|
|
843
|
+
:param unc_def: either xarray DataArray/Variable, or definition through tuple as ``(dims, data[, attrs])``. ``dims`` is a list of variable dimension names, ``data`` is a numpy array of uncertainty values and ``attrs`` is a dictionary of variable attributes. ``attrs`` should include an element ``pdf_shape`` which defines the uncertainty probability density function form, and ``err_corr`` which defines the error-correlation structure of the data. If omitted ``pdf_shape`` is assumed Gaussian and the error-correlation is assumed random.
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
# add uncertainty variable
|
|
847
|
+
if type(unc_def) == xr.DataArray:
|
|
848
|
+
self._obj[unc_var] = unc_def
|
|
849
|
+
|
|
850
|
+
# use templater functionality if var defined with tuple
|
|
851
|
+
elif type(unc_def) == tuple:
|
|
852
|
+
|
|
853
|
+
attrs = {}
|
|
854
|
+
if len(unc_def) == 3:
|
|
855
|
+
attrs = unc_def[2]
|
|
856
|
+
|
|
857
|
+
if "err_corr" not in attrs:
|
|
858
|
+
attrs["err_corr"] = []
|
|
859
|
+
|
|
860
|
+
var_attrs = {
|
|
861
|
+
"dtype": unc_def[1].dtype,
|
|
862
|
+
"dim": unc_def[0],
|
|
863
|
+
"attributes": attrs,
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
size = {d: s for d, s in zip(unc_def[0], unc_def[1].shape)}
|
|
867
|
+
|
|
868
|
+
var = create_var(unc_var, var_attrs, size)
|
|
869
|
+
var.values = unc_def[1]
|
|
870
|
+
|
|
871
|
+
self._obj[unc_var] = var
|
|
872
|
+
|
|
873
|
+
# add variable to uncertainty components attribute
|
|
874
|
+
if self._is_obs_var(obs_var):
|
|
875
|
+
if type(self._obj[obs_var].attrs["unc_comps"]) == str:
|
|
876
|
+
self._obj[obs_var].attrs["unc_comps"] = [
|
|
877
|
+
self._obj[obs_var].attrs["unc_comps"]
|
|
878
|
+
]
|
|
879
|
+
|
|
880
|
+
if unc_var not in self._obj[obs_var].attrs["unc_comps"]:
|
|
881
|
+
self._obj[obs_var].attrs["unc_comps"].append(unc_var)
|
|
882
|
+
|
|
883
|
+
else:
|
|
884
|
+
self._obj[obs_var].attrs["unc_comps"] = unc_var
|
|
885
|
+
|
|
886
|
+
def _remove_unc_var(self, obs_var: str, unc_var: str) -> None:
|
|
887
|
+
"""
|
|
888
|
+
Removes uncertainty variable from dataset
|
|
889
|
+
|
|
890
|
+
:param obs_var: observation variable name
|
|
891
|
+
:param unc_var: uncertainty variable name
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
del self._obj[unc_var]
|
|
895
|
+
self._obj[obs_var].attrs["unc_comps"].remove(unc_var)
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
if __name__ == "__main__":
|
|
899
|
+
pass
|