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.
@@ -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