cfapyx 2024.9.0__tar.gz

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 @@
1
+ from .backend import CFANetCDFBackendEntrypoint
@@ -0,0 +1,175 @@
1
+ __author__ = "Daniel Westwood"
2
+ __contact__ = "daniel.westwood@stfc.ac.uk"
3
+ __copyright__ = "Copyright 2024 United Kingdom Research and Innovation"
4
+
5
+ from xarray.backends import StoreBackendEntrypoint, BackendEntrypoint
6
+ from xarray.backends.common import AbstractDataStore
7
+ from xarray.core.dataset import Dataset
8
+ from xarray import conventions
9
+
10
+ from CFAPyX.datastore import CFADataStore
11
+
12
+ def open_cfa_dataset(
13
+ filename_or_obj,
14
+ drop_variables=None,
15
+ mask_and_scale=None,
16
+ decode_times=None,
17
+ concat_characters=None,
18
+ decode_coords=None,
19
+ use_cftime=None,
20
+ decode_timedelta=None,
21
+ cfa_options={},
22
+ group=None,
23
+ ):
24
+ """
25
+ Top-level function which opens a CFA dataset using Xarray. Creates a CFA Datastore
26
+ from the ``filename_or_obj`` provided, then passes this to a CFA StoreBackendEntrypoint
27
+ to create an Xarray Dataset. Most parameters are not handled by CFA, so only the
28
+ CFA-relevant ones are described here.
29
+
30
+ :param filename_or_obj: (str) The path to a CFA-netCDF file to be opened by Xarray
31
+
32
+ :param cfa_options: (dict) A set of kwargs provided to CFA which provide additional
33
+ configurations. Currently implemented are: substitutions (dict),
34
+ decode_cfa (bool)
35
+
36
+ :param group: (str) The name or path to a NetCDF group. CFA can handle opening
37
+ from specific groups and will inherit both ``group`` and ``global``
38
+ dimensions/attributes.
39
+
40
+ :returns: An xarray.Dataset object composed of xarray.DataArray objects representing the different
41
+ NetCDF variables and dimensions. CFA aggregated variables are decoded unless the ``decode_cfa``
42
+ parameter in ``cfa_options`` is false.
43
+ """
44
+
45
+ # Load the CFA datastore from the provided file (object not supported).
46
+ store = CFADataStore.open(filename_or_obj, group=group)
47
+
48
+ # Expands cfa_options into individual kwargs for the store.
49
+ store.cfa_options = cfa_options
50
+
51
+ use_active = False
52
+ if hasattr(store, 'use_active'):
53
+ use_active = store.use_active
54
+
55
+ # Xarray makes use of StoreBackendEntrypoints to provide the Dataset 'ds'
56
+ store_entrypoint = CFAStoreBackendEntrypoint()
57
+ ds = store_entrypoint.open_dataset(
58
+ store,
59
+ mask_and_scale=mask_and_scale,
60
+ decode_times=decode_times,
61
+ concat_characters=concat_characters,
62
+ decode_coords=decode_coords,
63
+ drop_variables=drop_variables,
64
+ use_cftime=use_cftime,
65
+ decode_timedelta=decode_timedelta,
66
+ use_active=use_active
67
+ )
68
+
69
+ return ds
70
+
71
+ class CFANetCDFBackendEntrypoint(BackendEntrypoint):
72
+
73
+ description = "Open CFA-netCDF files (.nca) using CFA-PyX in Xarray"
74
+ url = "https://cedadev.github.io/CFAPyX/"
75
+
76
+ def open_dataset(
77
+ self,
78
+ filename_or_obj,
79
+ *,
80
+ drop_variables=None,
81
+ mask_and_scale=None,
82
+ decode_times=None,
83
+ concat_characters=None,
84
+ decode_coords=None,
85
+ use_cftime=None,
86
+ decode_timedelta=None,
87
+ cfa_options={},
88
+ group=None,
89
+ # backend specific keyword arguments
90
+ # do not use 'chunks' or 'cache' here
91
+ ):
92
+ """
93
+ Returns a complete xarray representation of a CFA-netCDF dataset which includes expanding/decoding
94
+ CFA aggregated variables into proper arrays.
95
+ """
96
+
97
+ return open_cfa_dataset(
98
+ filename_or_obj,
99
+ drop_variables=drop_variables,
100
+ mask_and_scale=mask_and_scale,
101
+ decode_times=decode_times,
102
+ concat_characters=concat_characters,
103
+ decode_coords=decode_coords,
104
+ use_cftime=use_cftime,
105
+ decode_timedelta=decode_timedelta,
106
+ cfa_options=cfa_options,
107
+ group=group)
108
+
109
+ class CFAStoreBackendEntrypoint(StoreBackendEntrypoint):
110
+ description = "Open CFA-based Abstract Data Store"
111
+ url = "https://cedadev.github.io/CFAPyX/"
112
+
113
+ def open_dataset(
114
+ self,
115
+ cfa_xarray_store,
116
+ *,
117
+ mask_and_scale=True,
118
+ decode_times=True,
119
+ concat_characters=True,
120
+ decode_coords=True,
121
+ drop_variables=None,
122
+ use_cftime=None,
123
+ decode_timedelta=None,
124
+ use_active=False,
125
+ ) -> Dataset:
126
+ """
127
+ Takes cfa_xarray_store of type AbstractDataStore and creates an xarray.Dataset object.
128
+ Most parameters are not handled by CFA, so only the CFA-relevant ones are described here.
129
+
130
+ :param cfa_xarray_store: (obj) The CFA Datastore object which loads and decodes CFA
131
+ aggregated variables and dimensions.
132
+
133
+ :returns: An xarray.Dataset object composed of xarray.DataArray objects representing the different
134
+ NetCDF variables and dimensions. CFA aggregated variables are decoded unless the ``decode_cfa``
135
+ parameter in ``cfa_options`` is false.
136
+
137
+ """
138
+ assert isinstance(cfa_xarray_store, AbstractDataStore)
139
+
140
+ # Same as NetCDF4 operations, just with the CFA Datastore
141
+ vars, attrs = cfa_xarray_store.load()
142
+ encoding = cfa_xarray_store.get_encoding()
143
+
144
+ # Ensures variables/attributes comply with CF conventions.
145
+ vars, attrs, coord_names = conventions.decode_cf_variables(
146
+ vars,
147
+ attrs,
148
+ mask_and_scale=mask_and_scale,
149
+ decode_times=decode_times,
150
+ concat_characters=concat_characters,
151
+ decode_coords=decode_coords,
152
+ drop_variables=drop_variables,
153
+ use_cftime=use_cftime,
154
+ decode_timedelta=decode_timedelta,
155
+ )
156
+
157
+ # Create the xarray.Dataset object here.
158
+ if use_active:
159
+ try:
160
+ from XarrayActive import ActiveDataset
161
+
162
+ ds = ActiveDataset(vars, attrs=attrs)
163
+ except ImportError:
164
+ raise ImportError(
165
+ '"ActiveDataset" from XarrayActive failed to import - please '
166
+ 'ensure you have the XarrayActive package installed.'
167
+ )
168
+ else:
169
+ ds = Dataset(vars, attrs=attrs)
170
+
171
+ ds = ds.set_coords(coord_names.intersection(vars))
172
+ ds.set_close(cfa_xarray_store.close)
173
+ ds.encoding = encoding
174
+
175
+ return ds
@@ -0,0 +1,459 @@
1
+ __author__ = "Daniel Westwood"
2
+ __contact__ = "daniel.westwood@stfc.ac.uk"
3
+ __copyright__ = "Copyright 2024 United Kingdom Research and Innovation"
4
+
5
+ from xarray.backends import (
6
+ NetCDF4DataStore
7
+ )
8
+
9
+ from xarray.core.utils import FrozenDict
10
+ from xarray.core import indexing
11
+ from xarray.coding.variables import pop_to
12
+ from xarray.core.variable import Variable
13
+
14
+ import netCDF4
15
+ import numpy as np
16
+ import re
17
+
18
+ from CFAPyX.wrappers import FragmentArrayWrapper
19
+ from CFAPyX.decoder import get_fragment_positions, get_fragment_extents
20
+ from CFAPyX.group import CFAGroupWrapper
21
+
22
+
23
+ xarray_subs = {
24
+ 'file:///':'/'
25
+ }
26
+
27
+ class CFADataStore(NetCDF4DataStore):
28
+
29
+ """
30
+ DataStore container for the CFA-netCDF loaded file. Contains all unpacking routines
31
+ directly related to the specific variables and attributes. The ``NetCDF4Datastore``
32
+ Xarray class from which this class inherits, has an ``__init__`` method which
33
+ cannot easily be overriden, so properties are used instead for specific variables
34
+ that may be un-set at time of use.
35
+ """
36
+
37
+ @property
38
+ def chunks(self):
39
+ if hasattr(self,'_cfa_chunks'):
40
+ return self._cfa_chunks
41
+ return None
42
+
43
+ @chunks.setter
44
+ def chunks(self, value):
45
+ self._cfa_chunks = value
46
+
47
+ @property
48
+ def cfa_options(self):
49
+ """
50
+ Property of the datastore that relates private option variables to the standard
51
+ ``cfa_options`` parameter.
52
+ """
53
+
54
+ return {
55
+ 'substitutions': self._substitutions,
56
+ 'decode_cfa': self._decode_cfa,
57
+ 'chunks': self.chunks,
58
+ 'chunk_limits': self._chunk_limits,
59
+ 'use_active': self.use_active
60
+ }
61
+
62
+ @cfa_options.setter
63
+ def cfa_options(self, value):
64
+ self._set_cfa_options(**value)
65
+
66
+ def _set_cfa_options(
67
+ self,
68
+ substitutions=None,
69
+ decode_cfa=True,
70
+ chunks={},
71
+ chunk_limits=True,
72
+ use_active=False,
73
+ ):
74
+ """
75
+ Method to set cfa options.
76
+
77
+ :param substitutions: (dict) Set of provided substitutions to Xarray,
78
+ following the CFA conventions on substitutions.
79
+
80
+ :param decode_cfa: (bool) Optional setting to disable CFA decoding
81
+ in some cases, default is True.
82
+
83
+ :param use_active: (bool) Enable for use with XarrayActive.
84
+
85
+ :param chunks: (dict) Not implemented in 2024.9.0
86
+
87
+ :param chunk_limits: (dict) Not implemented in 2024.9.0
88
+ """
89
+
90
+ self.chunks = chunks
91
+ self._substitutions = substitutions
92
+ self._decode_cfa = decode_cfa
93
+ self._chunk_limits = chunk_limits
94
+ self.use_active = use_active
95
+
96
+ def _acquire(self, needs_lock=True):
97
+ """
98
+ Fetch the global or group dataset from the Datastore Caching Manager (NetCDF4)
99
+ """
100
+ with self._manager.acquire_context(needs_lock) as root:
101
+ ds = CFAGroupWrapper.open(root, self._group, self._mode)
102
+
103
+ self.conventions = ds.Conventions
104
+
105
+ return ds
106
+
107
+ def _decode_feature_data(self, feature_data, readd={}):
108
+ """
109
+ Decode the value of an object which is expected to be of the form of a
110
+ ``feature: variable`` blank-separated element list.
111
+ """
112
+ parts = re.split(': | ',feature_data)
113
+
114
+ # Anything that uses a ':' needs to be readded after the previous step.
115
+ for k, v in readd:
116
+ for p in parts:
117
+ p.replace(k,v)
118
+
119
+ return {k: v for k, v in zip(parts[0::2], parts[1::2])}
120
+
121
+ def _check_applied_conventions(self, agg_data):
122
+ """
123
+ Check that the aggregated data complies with the conventions specified in the
124
+ CFA-netCDF file
125
+ """
126
+
127
+ required = ('shape', 'location', 'address')
128
+ if 'CFA-0.6.2' in self.conventions.split(' '):
129
+ required = ('location', 'file', 'format')
130
+
131
+ for feature in required:
132
+ if feature not in agg_data:
133
+ raise ValueError(
134
+ f'CFA-netCDF file is not compliant with {self.conventions} '
135
+ f'Required aggregated data features: "{required}", '
136
+ f'Received "{tuple(agg_data.keys())}"'
137
+ )
138
+
139
+ def _perform_decoding(
140
+ self,
141
+ shape,
142
+ address,
143
+ location,
144
+ array_shape,
145
+ value=None,
146
+ cformat='',
147
+ substitutions=None):
148
+ """
149
+ Private method for performing the decoding of the standard ``fragment array
150
+ variables``. Any convention version-specific adjustments should be made prior
151
+ to decoding with this function, namely in the public method of the same name.
152
+
153
+ :param shape: (obj) The integer-valued ``shape`` fragment array variable
154
+ defines the shape of each fragment's data in its canonical
155
+ form. CF-1.12 section 2.8.1
156
+
157
+ :param address: (obj) The ``address`` fragment array variable, that may
158
+ have any data type, defines how to find each fragment
159
+ within its fragment dataset. CF-1.12 section 2.8.1
160
+
161
+ :param location: (obj) The string-valued ``location`` fragment array
162
+ variable defines the locations of fragment datasets using
163
+ Uniform Resource Identifiers (URIs). CF-1.12 section 2.8.1
164
+
165
+ :param value: (obj) *Optional* unique data value to fill a fragment array
166
+ where the data values within the fragment are all the same.
167
+
168
+ :param cformat: (str) *Optional* ``format`` argument if provided by the
169
+ CFA-netCDF or cfa-options parameters. CFA-0.6.2
170
+
171
+ :param substitutions: (dict) Set of substitutions to apply in the form 'base':'sub'
172
+
173
+ :returns: (fragment_info) A dictionary of fragment metadata where each
174
+ key is the coordinates of a fragment in index space and the
175
+ value is a dictionary of the attributes specific to that
176
+ fragment.
177
+
178
+ """
179
+
180
+ fragment_info = {}
181
+
182
+ # Extract non-padded fragment sizes per dimension.
183
+ fragment_size_per_dim = [i.compressed().tolist() for i in shape]
184
+
185
+ # Derive the total shape of the fragment array in all fragmented dimensions.
186
+ fragment_space = [len(fsize) for fsize in fragment_size_per_dim]
187
+
188
+ # Obtain the positions of each fragment in index space.
189
+ fragment_positions = get_fragment_positions(fragment_size_per_dim)
190
+
191
+ global_extent, extent, shapes = get_fragment_extents(
192
+ fragment_size_per_dim,
193
+ array_shape
194
+ )
195
+
196
+ if value is not None:
197
+ # --------------------------------------------------------
198
+ # This fragment contains a constant value, not file
199
+ # locations.
200
+ # --------------------------------------------------------
201
+ fragment_space = value.shape
202
+ fragment_info = {
203
+ frag_pos: {
204
+ "shape": shapes[frag_pos],
205
+ "fill_value": value[frag_pos].item(),
206
+ "global_extent": global_extent[frag_pos],
207
+ "extent": extent[frag_pos],
208
+ "format": "full",
209
+ }
210
+ for frag_pos in fragment_positions
211
+ }
212
+
213
+ return fragment_info, fragment_space
214
+
215
+ constructor_shape = location.shape
216
+
217
+ if not address.ndim: # Scalar address
218
+ addr = address.getValue()
219
+ adtype = np.array(addr).dtype
220
+ address = np.full(constructor_shape, addr, dtype=adtype)
221
+
222
+ if cformat != '':
223
+ if not cformat.ndim:
224
+ cft = cformat.getValue()
225
+ npdtype = np.array(cft).dtype
226
+ cformat = np.full(constructor_shape, cft, dtype=npdtype)
227
+
228
+ for frag_pos in fragment_positions:
229
+
230
+ fragment_info[frag_pos] = {
231
+ "shape" : shapes[frag_pos],
232
+ "location" : location[frag_pos],
233
+ "address" : address[frag_pos],
234
+ "extent" : extent[frag_pos],
235
+ "global_extent": global_extent[frag_pos]
236
+ }
237
+ if hasattr(cformat, 'shape'):
238
+ fragment_info[frag_pos]["format"] = cformat[frag_pos]
239
+
240
+ # Apply string substitutions to the fragment filenames
241
+ if substitutions:
242
+ for value in fragment_info.values():
243
+ for base, sub in substitutions.items():
244
+ value["location"] = value["location"].replace(base, sub)
245
+
246
+ return fragment_info, fragment_space
247
+
248
+ # Public class methods
249
+
250
+ def perform_decoding(self, array_shape, agg_data):
251
+ """
252
+ Public method ``perform_decoding`` involves extracting the aggregated
253
+ information parameters and assembling the required information for actual
254
+ decoding.
255
+ """
256
+
257
+ # If not raised an error in checking, we can continue.
258
+ self._check_applied_conventions(agg_data)
259
+
260
+ cformat = ''
261
+ value = None
262
+ try:
263
+ if 'CFA-0.6.2' in self.conventions:
264
+ shape = self.ds.variables[agg_data['location']]
265
+ location = self.ds.variables[agg_data['file']]
266
+ cformat = self.ds.variables[agg_data['format']]
267
+ else: # Default to CF-1.12
268
+ shape = self.ds.variables[agg_data['shape']]
269
+ location = self.ds.variables[agg_data['location']]
270
+ if 'value' in agg_data:
271
+ value = self.ds.variables[agg_data['value']]
272
+
273
+ address = self.ds.variables[agg_data['address']]
274
+ except:
275
+ raise ValueError(
276
+ 'One or more aggregated data features specified could not be '
277
+ 'found in the data: '
278
+ f'"{tuple(agg_data.keys())}"'
279
+ )
280
+
281
+ subs = {}
282
+ if hasattr(location, 'substitutions'):
283
+ subs = location.substitutions.replace('https://', 'https@//')
284
+ subs = self._decode_feature_data(subs, readd={'https://':'https@//'})
285
+
286
+ return self._perform_decoding(shape, address, location, array_shape,
287
+ cformat=cformat, value=value,
288
+ substitutions = xarray_subs | subs)
289
+ # Combine substitutions with known defaults for using in xarray.
290
+
291
+ def get_variables(self):
292
+ """
293
+ Fetch the netCDF4.Dataset variables and perform some CFA decoding if
294
+ necessary.
295
+
296
+ ``ds`` is now a ``GroupedDatasetWrapper`` object from ``CFAPyX.group`` which
297
+ has flattened the group structure and allows fetching of variables and
298
+ attributes from the whole group tree from which a specific group may inherit.
299
+
300
+ :returns: A ``FrozenDict`` Xarray object of the names of all variables,
301
+ and methods to fetch those variables, depending on if those
302
+ variables are standard NetCDF4 or CFA Aggregated variables.
303
+ """
304
+
305
+ if not self._decode_cfa:
306
+ return FrozenDict(
307
+ (k, self.open_variable(k, v)) for k, v in self.ds.variables.items()
308
+ )
309
+
310
+ # Determine CFA-aggregated variables
311
+ all_vars, real_vars = {}, {}
312
+
313
+ fragment_array_vars = []
314
+
315
+ ## Ignore variables in the set of standardised terms.
316
+ for avar in self.ds.variables.keys():
317
+ cfa = False
318
+ ## CF-Compliant method of identifying aggregated variables.
319
+ if hasattr(self.ds.variables[avar], 'aggregated_dimensions'):
320
+ cfa = True
321
+
322
+ agg_data = self.ds.variables[avar].aggregated_data.split(' ')
323
+
324
+ for vname in agg_data:
325
+ fragment_array_vars += re.split(': | ',vname)
326
+
327
+ all_vars[avar] = (self.ds.variables[avar], cfa)
328
+
329
+ # Ignore fragment array variables at this stage of decoding.
330
+ for var in all_vars.keys():
331
+ if var not in fragment_array_vars:
332
+ real_vars[var] = all_vars[var]
333
+
334
+
335
+ return FrozenDict(
336
+ (k, self.open_variable(k, v)) for k, v in real_vars.items()
337
+ )
338
+
339
+ def get_attrs(self):
340
+ """
341
+ Produce the FrozenDict of attributes from the ``NetCDF4.Dataset`` or
342
+ ``CFAGroupWrapper`` in the case of using a group or nested group tree.
343
+ """
344
+ return FrozenDict((k, self.ds.getncattr(k)) for k in self.ds.ncattrs())
345
+
346
+ def open_variable(self, name: str, var):
347
+ """
348
+ Open a CFA-netCDF variable as either a standard NetCDF4 Datastore variable
349
+ or as a CFA aggregated variable which requires additional decoding.
350
+
351
+ :param name: (str) A named NetCDF4 variable.
352
+
353
+ :param var: (obj) The NetCDF4.Variable object or a tuple with the contents
354
+ ``(NetCDF4.Variable, cfa)`` where ``cfa`` is a bool that
355
+ determines if the variable is a CFA or standard variable.
356
+
357
+ :returns: The variable object opened as either a standard store variable
358
+ or CFA aggregated variable.
359
+ """
360
+ if type(var) == tuple:
361
+ if var[1] and self._decode_cfa:
362
+ variable = self.open_cfa_variable(name, var[0])
363
+ else:
364
+ variable = self.open_store_variable(name, var[0])
365
+ else:
366
+ variable = self.open_store_variable(name, var)
367
+ return variable
368
+
369
+ def open_cfa_variable(self, name: str, var):
370
+ """
371
+ Open a CFA Aggregated variable with the correct parameters to create an
372
+ Xarray ``Variable`` instance.
373
+
374
+ :param name: (str) A named NetCDF4 variable.
375
+
376
+ :param var: (obj) The NetCDF4.Variable object or a tuple with the
377
+ contents ``(NetCDF4.Variable, cfa)`` where ``cfa`` is
378
+ a bool that determines if the variable is a CFA or
379
+ standard variable.
380
+
381
+ :returns: An xarray ``Variable`` instance constructed from the
382
+ attributes provided here, and data provided by a
383
+ ``FragmentArrayWrapper`` which is indexed by Xarray's
384
+ ``LazilyIndexedArray`` class.
385
+ """
386
+
387
+ real_dims = {
388
+ d: self.ds.dimensions[d].size for d in var.aggregated_dimensions.split(' ')
389
+ }
390
+ agg_data = self._decode_feature_data(var.aggregated_data)
391
+
392
+ ## Array Metadata
393
+ dimensions = tuple(real_dims.keys())
394
+ array_shape = tuple(real_dims.values())
395
+
396
+ fragment_info, fragment_space = self.perform_decoding(array_shape, agg_data)
397
+
398
+ units = ''
399
+ if hasattr(var, 'units'):
400
+ units = getattr(var, 'units')
401
+ if hasattr(var, 'aggregated_units'):
402
+ units = getattr(var, 'aggregated_units')
403
+
404
+ ## Get non-aggregated attributes.
405
+ attributes = {}
406
+ for k in var.ncattrs():
407
+ if 'aggregated' not in k:
408
+ attributes[k] = var.getncattr(k)
409
+
410
+ ## Array-like object
411
+ data = indexing.LazilyIndexedArray(
412
+ FragmentArrayWrapper(
413
+ fragment_info,
414
+ fragment_space,
415
+ shape=array_shape,
416
+ units=units,
417
+ dtype=var.dtype,
418
+ cfa_options=self.cfa_options,
419
+ named_dims=dimensions,
420
+ ))
421
+
422
+ encoding = {}
423
+ if isinstance(var.datatype, netCDF4.EnumType):
424
+ encoding["dtype"] = np.dtype(
425
+ data.dtype,
426
+ metadata={
427
+ "enum": var.datatype.enum_dict,
428
+ "enum_name": var.datatype.name,
429
+ },
430
+ )
431
+ else:
432
+ encoding["dtype"] = var.dtype
433
+
434
+ if data.dtype.kind == "S" and "_FillValue" in attributes:
435
+ attributes["_FillValue"] = np.bytes_(attributes["_FillValue"])
436
+
437
+ filters = var.filters()
438
+ if filters is not None:
439
+ encoding.update(filters)
440
+ chunking = var.chunking()
441
+ if chunking is not None:
442
+ if chunking == "contiguous":
443
+ encoding["contiguous"] = True
444
+ encoding["chunksizes"] = None
445
+ else:
446
+ encoding["contiguous"] = False
447
+ encoding["chunksizes"] = tuple(chunking)
448
+ encoding["preferred_chunks"] = dict(zip(var.dimensions, chunking))
449
+ # TODO: figure out how to round-trip "endian-ness" without raising
450
+ # warnings from netCDF4
451
+ # encoding['endian'] = var.endian()
452
+ pop_to(attributes, encoding, "least_significant_digit")
453
+ # save source so __repr__ can detect if it's local or not
454
+ encoding["source"] = self._filename
455
+ encoding["original_shape"] = data.shape
456
+
457
+ v = Variable(dimensions, data, attributes, encoding)
458
+ return v
459
+