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,659 @@
1
+ """
2
+ Utilities for creating xarray dataset variables in specified forms
3
+ """
4
+
5
+ import string
6
+ from copy import deepcopy
7
+ import xarray
8
+ import numpy
9
+ from typing import Union, Optional, List, Dict, Tuple
10
+
11
+
12
+ __author__ = "Sam Hunt <sam.hunt@npl.co.uk>"
13
+
14
+
15
+ DEFAULT_DIM_NAMES = list(string.ascii_lowercase[-3:]) + list(
16
+ string.ascii_lowercase[:-3]
17
+ )
18
+ DEFAULT_DIM_NAMES.reverse()
19
+
20
+
21
+ ERR_CORR_DEFS = {
22
+ "random": {"n_params": 0},
23
+ "rectangle_absolute": {"n_params": 2},
24
+ "rectangular_relative": {"n_params": 2},
25
+ "triangular_relative": {"n_params": 2},
26
+ }
27
+
28
+
29
+ class DatasetUtil:
30
+ """
31
+ Class to provide utilities for generating standard xarray DataArrays and Variables
32
+ """
33
+
34
+ @staticmethod
35
+ def create_default_array(
36
+ dim_sizes: List[int],
37
+ dtype: numpy.typecodes,
38
+ dim_names: Optional[List[str]] = None,
39
+ fill_value: Optional[Union[int, float]] = None,
40
+ ) -> xarray.DataArray:
41
+ """
42
+ Return default empty xarray DataArray
43
+
44
+ :param dim_sizes: dimension sizes, i.e. ``[dim1_size, dim2_size, dim3_size]`` (e.g. ``[2,3,5]``)
45
+ :param dtype: numpy data type
46
+ :param dim_names: dimension names, i.e. ``["dim1_name", "dim2_name", "dim3_name"]``
47
+ :param fill_value: fill value (if None CF compliant value used)
48
+
49
+ :returns: Default empty array
50
+ """
51
+
52
+ if fill_value is None:
53
+ fill_value = DatasetUtil.get_default_fill_value(dtype)
54
+
55
+ empty_array = numpy.full(dim_sizes, fill_value, dtype)
56
+
57
+ if dim_names is not None:
58
+ default_array = xarray.DataArray(empty_array, dims=dim_names)
59
+ else:
60
+ default_array = xarray.DataArray(
61
+ empty_array, dims=DEFAULT_DIM_NAMES[-len(dim_sizes) :]
62
+ )
63
+
64
+ return default_array
65
+
66
+ @staticmethod
67
+ def create_variable(
68
+ dim_sizes: List[int],
69
+ dtype: numpy.typecodes,
70
+ dim_names: Optional[List[str]] = None,
71
+ attributes: Dict = None,
72
+ fill_value: Optional[Union[int, float]] = None,
73
+ ) -> xarray.Variable:
74
+ """
75
+ Return default empty xarray Variable
76
+
77
+ :param dim_sizes: dimension sizes, i.e. ``[dim1_size, dim2_size, dim3_size]`` (e.g. ``[2,3,5]``)
78
+ :param dtype: numpy data type
79
+ :param dim_names: dimension names as strings, i.e. ``["dim1_name", "dim2_name", "dim3_size"]``
80
+ :param attributes: dictionary of variable attributes, e.g. standard_name
81
+ :param fill_value: fill value (if None CF compliant value used)
82
+
83
+ :returns: Default empty variable
84
+ """
85
+
86
+ if fill_value is None:
87
+ fill_value = DatasetUtil.get_default_fill_value(dtype)
88
+
89
+ default_array = DatasetUtil.create_default_array(
90
+ dim_sizes, dtype, dim_names, fill_value=fill_value
91
+ )
92
+
93
+ if dim_names is None:
94
+ variable = xarray.Variable(
95
+ DEFAULT_DIM_NAMES[-len(dim_sizes) :], default_array
96
+ )
97
+ else:
98
+ variable = xarray.Variable(dim_names, default_array)
99
+
100
+ variable.attrs["_FillValue"] = fill_value
101
+
102
+ if attributes is not None:
103
+ variable.attrs = {**variable.attrs, **attributes}
104
+
105
+ return variable
106
+
107
+ @staticmethod
108
+ def create_unc_variable(
109
+ dim_sizes: List[int],
110
+ dtype: numpy.typecodes,
111
+ dim_names: List[str],
112
+ attributes: Optional[Dict] = None,
113
+ pdf_shape: str = "gaussian",
114
+ err_corr: Optional[List[Dict[str, Union[str, List]]]] = None,
115
+ ) -> xarray.Variable:
116
+ """
117
+ Return default empty 1d xarray uncertainty Variable
118
+
119
+ :param dim_sizes: dimension sizes, i.e. ``[dim1_size, dim2_size, dim3_size]`` (e.g. ``[2,3,5]``)
120
+ :param dtype: data type
121
+ :param dim_names: dimension names, i.e. ``["dim1_name", "dim2_name", "dim3_size"]``
122
+ :param attributes: dictionary of variable attributes, e.g. standard_name
123
+ :param pdf_shape: (default: `"gaussian"`) pdf shape of uncertainties
124
+ :param err_corr: uncertainty error-correlation structure definition, defined as below.
125
+
126
+ :returns: Default empty flag vector variable
127
+
128
+ Each element of ``err_corr`` is a dictionary that defines the error-correlation along one or more dimensions,
129
+ which should include the following entries:
130
+
131
+ * ``dim`` (*str*/*list*) - name of the dimension(s) as a ``str`` or list of ``str``'s (i.e. from ``dim_names``)
132
+ * ``form`` (*str*) - error-correlation form name, functional form of error-correlation structure for
133
+ dimension(s)
134
+ * ``params`` (*list*) - (optional) parameters of the error-correlation structure defining function for dimension
135
+ if required. The number of parameters required depends on the particular form.
136
+ * ``units`` (*list*) - (optional) units of the error-correlation function parameters for dimension
137
+ (ordered as the parameters)
138
+
139
+ For more information on the required form of these entries, see the :ref:`uncertainties section <err corr>` of
140
+ the user guide.
141
+
142
+ .. note::
143
+ If the error-correlation structure is not defined along a particular dimension (i.e. it is not
144
+ included in ``err_corr``), the error-correlation is assumed random. Variable attributes are
145
+ populated to the effect of this assumption.
146
+ """
147
+
148
+ # define uncertainty variable attributes, based on FIDUCEO Full FCDR definition (if required)
149
+ attributes = {} if attributes is None else attributes
150
+
151
+ if err_corr is None:
152
+ err_corr = []
153
+
154
+ # set undefined dims as random
155
+ defined_err_corr_dims = []
156
+ for erd in err_corr:
157
+ if isinstance(erd["dim"], str):
158
+ defined_err_corr_dims.append(erd["dim"])
159
+ else:
160
+ defined_err_corr_dims.extend(erd["dim"])
161
+
162
+ missing_err_corr_dims = [
163
+ dim for dim in dim_names if dim not in defined_err_corr_dims
164
+ ]
165
+ for missing_err_corr_dim in missing_err_corr_dims:
166
+ err_corr.append({"dim": missing_err_corr_dim, "form": "random"})
167
+
168
+ for i, ecdef in enumerate(err_corr):
169
+ idx = str(i + 1)
170
+
171
+ dim_str = DatasetUtil.return_err_corr_dim_str(idx)
172
+ form_str = DatasetUtil.return_err_corr_form_str(idx)
173
+ params_str = DatasetUtil.return_err_corr_params_str(idx)
174
+ units_str = DatasetUtil.return_err_corr_units_str(idx)
175
+
176
+ form = ecdef["form"]
177
+ if isinstance(ecdef["dim"], list) and len(ecdef["dim"]) == 1:
178
+ attributes[dim_str] = ecdef["dim"]
179
+ else:
180
+ attributes[dim_str] = ecdef["dim"]
181
+ attributes[form_str] = ecdef["form"]
182
+ attributes[units_str] = ecdef["units"] if "units" in ecdef else []
183
+
184
+ # if defined form, check number of params valid
185
+ if "params" in ecdef:
186
+ if form in ERR_CORR_DEFS.keys():
187
+ n_params = len(ecdef["params"])
188
+ req_n_params = ERR_CORR_DEFS[form]["n_params"]
189
+ if n_params != req_n_params:
190
+ raise ValueError(
191
+ "Must define "
192
+ + str(req_n_params)
193
+ + " for correlation form"
194
+ + form
195
+ + "(not "
196
+ + str(n_params)
197
+ + ")"
198
+ )
199
+
200
+ attributes[params_str] = ecdef["params"]
201
+
202
+ else:
203
+ attributes[params_str] = []
204
+
205
+ attributes["pdf_shape"] = pdf_shape
206
+
207
+ # Create variable
208
+ variable = DatasetUtil.create_variable(
209
+ dim_sizes,
210
+ dtype,
211
+ dim_names=dim_names,
212
+ attributes=attributes,
213
+ )
214
+
215
+ return variable
216
+
217
+ @staticmethod
218
+ def return_err_corr_dim_str(idx):
219
+ return "_".join(["err", "corr", idx, "dim"])
220
+
221
+ @staticmethod
222
+ def return_err_corr_form_str(idx):
223
+ return "_".join(["err", "corr", idx, "form"])
224
+
225
+ @staticmethod
226
+ def return_err_corr_params_str(idx):
227
+ return "_".join(["err", "corr", idx, "params"])
228
+
229
+ @staticmethod
230
+ def return_err_corr_units_str(idx):
231
+ return "_".join(["err", "corr", idx, "units"])
232
+
233
+ @staticmethod
234
+ def create_flags_variable(
235
+ dim_sizes: List[int],
236
+ meanings: List[str],
237
+ dim_names: Optional[List[str]] = None,
238
+ attributes: Optional[Dict] = None,
239
+ ) -> xarray.Variable:
240
+ """
241
+ Return default empty 1d xarray flag Variable
242
+
243
+ :param dim_sizes: dimension sizes, i.e. ``[dim1_size, dim2_size, dim3_size]`` (e.g. ``[2,3,5]``)
244
+ :param meanings: flag meanings by bit
245
+ :param dim_names: dimension names, i.e. ``["dim1_name", "dim2_name", "dim3_size"]``
246
+ :param attributes: dictionary of variable attributes, e.g. standard_name
247
+
248
+ :returns: Default empty flag vector variable
249
+ """
250
+
251
+ n_masks = len(meanings)
252
+
253
+ data_type = DatasetUtil.return_flags_dtype(n_masks)
254
+
255
+ variable = DatasetUtil.create_variable(
256
+ dim_sizes,
257
+ data_type,
258
+ dim_names=dim_names,
259
+ attributes=attributes,
260
+ )
261
+
262
+ # initialise flags to zero (instead of fillvalue)
263
+ variable.values = 0 * variable.values
264
+
265
+ # add flag attributes
266
+ variable.attrs.update(DatasetUtil.pack_flag_attrs(meanings))
267
+
268
+ # todo - make sure flags can't have units
269
+
270
+ return variable
271
+
272
+ @staticmethod
273
+ def pack_flag_attrs(
274
+ flag_meanings: List[str], flag_masks: Optional[list] = None
275
+ ) -> dict:
276
+ """
277
+ Derive flag related dataset attributes
278
+
279
+ :param flag_meanings: flag meanings
280
+ :param flag_masks: pre-defined flag masks, generated if not provided
281
+ :return: set of derived flag related attributes
282
+ """
283
+
284
+ n_masks = len(flag_meanings)
285
+
286
+ flag_attrs = dict()
287
+
288
+ flag_attrs["flag_meanings"] = (
289
+ str(flag_meanings)[1:-1].replace("'", "").replace(",", "")
290
+ )
291
+
292
+ if flag_masks is None:
293
+ flag_masks = [2**i for i in range(0, n_masks)]
294
+
295
+ flag_attrs["flag_masks"] = str(flag_masks)[1:-1]
296
+
297
+ return flag_attrs
298
+
299
+ @staticmethod
300
+ def unpack_flag_attrs(attrs: dict) -> Tuple[list, list]:
301
+ """
302
+ Extract flag related metadata from dataset attributes
303
+
304
+ :param attrs: flag variable attributes
305
+ :return: flag meanings, flag masks lists
306
+ """
307
+
308
+ flag_meanings = (
309
+ attrs["flag_meanings"].split() if "flag_meanings" in attrs else []
310
+ )
311
+ flag_mask = attrs["flag_masks"].split(",") if "flag_masks" in attrs else []
312
+ flag_mask = [int(m) for m in flag_mask] if flag_mask != [""] else []
313
+
314
+ return flag_meanings, flag_mask
315
+
316
+ @staticmethod
317
+ def add_flag_meaning_to_attrs(
318
+ attrs: dict, flag_meaning: str, dtype: numpy.typecodes
319
+ ) -> dict:
320
+ """
321
+ Add new meaning to flag variable attributes
322
+
323
+ :param attrs: flag variable attributes
324
+ :param flag_meaning: new flag name
325
+ :param dtype: flag variable dtype
326
+
327
+ :return: updated variable attributes
328
+ """
329
+
330
+ updated_attrs = deepcopy(attrs)
331
+
332
+ flag_meanings, flag_masks = DatasetUtil.unpack_flag_attrs(attrs)
333
+
334
+ # check if variable for available flags
335
+ max_n_flags = numpy.iinfo(dtype).bits
336
+ all_flag_masks = [2**i for i in range(0, max_n_flags)]
337
+ available_flag_masks = list(set(all_flag_masks) - set(flag_masks))
338
+
339
+ if not available_flag_masks:
340
+ raise ValueError(
341
+ "cannot assign any more masks to variable with dtype" + str(dtype)
342
+ )
343
+
344
+ flag_mask = min(available_flag_masks)
345
+
346
+ updated_attrs["flag_meanings"] = (
347
+ updated_attrs["flag_meanings"] + " " + flag_meaning
348
+ if "flag_meanings" in updated_attrs
349
+ else flag_meaning
350
+ )
351
+ updated_attrs["flag_masks"] = (
352
+ updated_attrs["flag_masks"] + ", " + str(flag_mask)
353
+ if "flag_masks" in updated_attrs
354
+ else str(flag_mask)
355
+ )
356
+
357
+ return updated_attrs
358
+
359
+ @staticmethod
360
+ def rm_flag_meaning_from_attrs(attrs: dict, flag_meaning: str) -> dict:
361
+ """
362
+ Remove flag meaning from flag variable attributes
363
+
364
+ :param attrs: flag variable attributes
365
+ :param flag_meaning: new flag name
366
+
367
+ :return: updated variable attributes
368
+ """
369
+
370
+ updated_attrs = deepcopy(attrs)
371
+
372
+ flag_meanings, flag_masks = DatasetUtil.unpack_flag_attrs(attrs)
373
+
374
+ if flag_meaning in flag_meanings:
375
+ i_meaning = flag_meanings.index(flag_meaning)
376
+ else:
377
+ raise ValueError("no flag " + flag_meaning)
378
+
379
+ del flag_meanings[i_meaning]
380
+ del flag_masks[i_meaning]
381
+
382
+ updated_attrs.update(DatasetUtil.pack_flag_attrs(flag_meanings, flag_masks))
383
+
384
+ return updated_attrs
385
+
386
+ @staticmethod
387
+ def return_flags_dtype(n_masks: int) -> numpy.typecodes:
388
+ """
389
+ Return required flags array data type
390
+
391
+ :param n_masks: number of masks required in flag array
392
+ :return: data type
393
+ """
394
+
395
+ if n_masks <= 8:
396
+ return numpy.uint8
397
+ elif n_masks <= 16:
398
+ return numpy.uint16
399
+ elif n_masks <= 32:
400
+ return numpy.uint32
401
+ else:
402
+ return numpy.uint64
403
+
404
+ @staticmethod
405
+ def add_encoding(
406
+ variable: xarray.Variable,
407
+ dtype: numpy.typecodes,
408
+ scale_factor: Optional[float] = 1.0,
409
+ offset: Optional[float] = 0.0,
410
+ fill_value: Optional[Union[int, float]] = None,
411
+ chunksizes: Optional[float] = None,
412
+ ):
413
+ """
414
+ Add encoding to xarray Variable to apply when writing netCDF files
415
+
416
+ :param variable: data variable
417
+ :param dtype: numpy data type
418
+ :param scale_factor: variable scale factor
419
+ :param offset: variable offset value
420
+ :param fill_value: fill value
421
+ :param chunksizes: chucksizes
422
+ """
423
+
424
+ # todo - make sure flags can't have encoding added
425
+
426
+ encoding_dict = {
427
+ "dtype": dtype,
428
+ "scale_factor": scale_factor,
429
+ "add_offset": offset,
430
+ }
431
+
432
+ if chunksizes is not None:
433
+ encoding_dict.update({"chunksizes": chunksizes})
434
+
435
+ if fill_value is not None:
436
+ encoding_dict.update({"_FillValue": fill_value})
437
+ else:
438
+ encoding_dict.update(
439
+ {"_FillValue": DatasetUtil.get_default_fill_value(dtype)}
440
+ )
441
+
442
+ variable.encoding = encoding_dict
443
+
444
+ @staticmethod
445
+ def get_default_fill_value(dtype: numpy.typecodes) -> Union[int, float]:
446
+ """
447
+ Returns default fill_value for given data type
448
+
449
+ :param dtype: numpy data type
450
+
451
+ :return: CF-conforming fill value
452
+ """
453
+
454
+ if dtype == numpy.int8:
455
+ return numpy.int8(-127)
456
+ if dtype == numpy.uint8:
457
+ return numpy.iinfo(numpy.uint8).max
458
+ elif dtype == numpy.int16:
459
+ return numpy.int16(-32767)
460
+ elif dtype == numpy.uint16:
461
+ return numpy.iinfo(numpy.uint16).max
462
+ elif dtype == numpy.int32:
463
+ return numpy.int32(-2147483647)
464
+ elif dtype == numpy.uint32:
465
+ return numpy.iinfo(numpy.uint32).max
466
+ elif dtype == numpy.int64:
467
+ return numpy.int64(-9223372036854775806)
468
+ elif dtype == numpy.float32:
469
+ return numpy.float32(9.96921e36)
470
+ elif dtype == numpy.float64:
471
+ return numpy.float64(9.969209968386869e36)
472
+
473
+ @staticmethod
474
+ def _get_flag_encoding(da):
475
+ """
476
+ Returns flag encoding for flag type data array
477
+ :type da: xarray.DataArray
478
+ :param da: data array
479
+ :return: flag meanings
480
+ :rtype: list
481
+ :return: flag masks
482
+ :rtype: list
483
+ """
484
+
485
+ try:
486
+ flag_meanings = da.attrs["flag_meanings"].split()
487
+ flag_masks = [int(fm) for fm in da.attrs["flag_masks"].split(",")]
488
+ except KeyError:
489
+ raise KeyError(da.name + " not a flag variable")
490
+
491
+ return flag_meanings, flag_masks
492
+
493
+ @staticmethod
494
+ def unpack_flags(da):
495
+ """
496
+ Breaks down flag data array into dataset of boolean masks for each flag
497
+ :type da: xarray.DataArray
498
+ :param da: dataset
499
+ :return: flag masks
500
+ :rtype: xarray.Dataset
501
+ """
502
+
503
+ flag_meanings, flag_masks = DatasetUtil._get_flag_encoding(da)
504
+
505
+ ds = xarray.Dataset()
506
+ for flag_meaning, flag_mask in zip(flag_meanings, flag_masks):
507
+ ds[flag_meaning] = DatasetUtil.create_variable(
508
+ list(da.shape), bool, dim_names=list(da.dims)
509
+ )
510
+ ds[flag_meaning] = (da & flag_mask).astype(bool)
511
+
512
+ return ds
513
+
514
+ @staticmethod
515
+ def get_flags_mask_or(da, flags=None):
516
+ """
517
+ Returns boolean mask for set of flags, defined as logical or of flags
518
+
519
+ :type da: xarray.DataArray
520
+ :param da: dataset
521
+
522
+ :type flags: list
523
+ :param flags: list of flags (if unset all data flags selected)
524
+
525
+ :return: flag masks
526
+ :rtype: numpy.ndarray
527
+ """
528
+
529
+ flags_ds = DatasetUtil.unpack_flags(da)
530
+
531
+ flags = flags if flags is not None else flags_ds.data_vars
532
+ mask_flags = numpy.array([flags_ds[flag].values for flag in flags])
533
+
534
+ return numpy.logical_or.reduce(mask_flags)
535
+
536
+ @staticmethod
537
+ def get_flags_mask_and(da, flags=None):
538
+ """
539
+ Returns boolean mask for set of flags, defined as logical and of flags
540
+
541
+ :type da: xarray.DataArray
542
+ :param da: dataset
543
+
544
+ :type flags: list
545
+ :param flags: list of flags (if unset all data flags selected)
546
+
547
+ :return: flag masks
548
+ :rtype: numpy.ndarray
549
+ """
550
+
551
+ flags_ds = DatasetUtil.unpack_flags(da)
552
+
553
+ flags = flags if flags is not None else flags_ds.data_vars
554
+ mask_flags = numpy.array([flags_ds[flag].values for flag in flags])
555
+
556
+ return numpy.logical_and.reduce(mask_flags)
557
+
558
+ @staticmethod
559
+ def set_flag(da, flag_name, error_if_set=False):
560
+ """
561
+ Sets named flag for elements in data array
562
+ :type da: xarray.DataArray
563
+ :param da: dataset
564
+ :type flag_name: str
565
+ :param flag_name: name of flag to set
566
+ :type error_if_set: bool
567
+ :param error_if_set: raises error if chosen flag is already set for any element
568
+ """
569
+
570
+ set_flags = DatasetUtil.unpack_flags(da)[flag_name]
571
+
572
+ if numpy.any(set_flags == True) and error_if_set:
573
+ raise ValueError(
574
+ "Flag " + flag_name + " already set for variable " + da.name
575
+ )
576
+
577
+ # Find flag mask
578
+ flag_meanings, flag_masks = DatasetUtil._get_flag_encoding(da)
579
+ flag_bit = flag_meanings.index(flag_name)
580
+ flag_mask = flag_masks[flag_bit]
581
+
582
+ da.values = da.values | flag_mask
583
+
584
+ return da
585
+
586
+ @staticmethod
587
+ def unset_flag(da, flag_name, error_if_unset=False):
588
+ """
589
+ Unsets named flag for specified index of dataset variable
590
+ :type da: xarray.DataArray
591
+ :param da: data array
592
+ :type flag_name: str
593
+ :param flag_name: name of flag to unset
594
+ :type error_if_unset: bool
595
+ :param error_if_unset: raises error if chosen flag is already set at specified index
596
+ """
597
+
598
+ set_flags = DatasetUtil.unpack_flags(da)[flag_name]
599
+
600
+ if numpy.any(set_flags == False) and error_if_unset:
601
+ raise ValueError(
602
+ "Flag " + flag_name + " already set for variable " + da.name
603
+ )
604
+
605
+ # Find flag mask
606
+ flag_meanings, flag_masks = DatasetUtil._get_flag_encoding(da)
607
+ flag_bit = flag_meanings.index(flag_name)
608
+ flag_mask = flag_masks[flag_bit]
609
+
610
+ da.values = da.values & ~flag_mask
611
+
612
+ return da
613
+
614
+ @staticmethod
615
+ def get_set_flags(da):
616
+ """
617
+ Return list of set flags for single element data array
618
+ :type da: xarray.DataArray
619
+ :param da: single element data array
620
+ :return: set flags
621
+ :rtype: list
622
+ """
623
+
624
+ if da.shape != ():
625
+ raise ValueError("Must pass single element data array")
626
+
627
+ flag_meanings, flag_masks = DatasetUtil._get_flag_encoding(da)
628
+
629
+ set_flags = []
630
+ for flag_meaning, flag_mask in zip(flag_meanings, flag_masks):
631
+ if da & flag_mask:
632
+ set_flags.append(flag_meaning)
633
+
634
+ return set_flags
635
+
636
+ @staticmethod
637
+ def check_flag_set(da, flag_name):
638
+ """
639
+ Returns if flag for single element data array
640
+ :type da: xarray.DataArray
641
+ :param da: single element data array
642
+ :type flag_name: str
643
+ :param flag_name: name of flag to set
644
+ :return: set flags
645
+ :rtype: list
646
+ """
647
+
648
+ if da.shape != ():
649
+ raise ValueError("Must pass single element data array")
650
+
651
+ set_flags = DatasetUtil.get_set_flags(da)
652
+
653
+ if flag_name in set_flags:
654
+ return True
655
+ return False
656
+
657
+
658
+ if __name__ == "__main__":
659
+ pass