diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..04b965f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: Automatic Test +# Specify which GitHub events will trigger a CI build + +on: push +# Define a single job, build + +jobs: + build: + # Specify an OS for the runner + runs-on: ubuntu-latest + + #Define steps + steps: + + # Firstly, checkout repo + - name: Checkout repository + uses: actions/checkout@v2 + # Set up Python env + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + # Install dependencies + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + pip3 install -r requirements.txt + pip3 install -e . + # Test with pytest + - name: Run pytest + run: | + pytest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2fa467b..0757c3d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ cf_repos xarray_repos notes *.pyc -*ipynb_checkpoints* \ No newline at end of file +*ipynb_checkpoints* +CFAPyX/__pycache__/*.pyc +CFAPyX.egg-info/ +.vscode/ +testing/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index b38e0a9..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "testPlainAgg", - "request": "launch", - "type": "debugpy", - "program": "/home/users/dwest77/Documents/cfa_python_dw/tests/testPlainAgg.py", - "python":"/home/users/dwest77/Documents/cfa_python_dw/cf-python/lcf/bin/python", - "console": "integratedTerminal", - "justMyCode":false - } - ] -} \ No newline at end of file diff --git a/CFAPyX/__init__.py b/CFAPyX/__init__.py index 2cf9a83..3eb9432 100644 --- a/CFAPyX/__init__.py +++ b/CFAPyX/__init__.py @@ -1 +1 @@ -from .backendentrypoint import CFANetCDFBackendEntrypoint \ No newline at end of file +from .backend import CFANetCDFBackendEntrypoint \ No newline at end of file diff --git a/CFAPyX/__pycache__/__init__.cpython-311.pyc b/CFAPyX/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index f9a3b3a..0000000 Binary files a/CFAPyX/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/CFAPyX/__pycache__/backendentrypoint.cpython-311.pyc b/CFAPyX/__pycache__/backendentrypoint.cpython-311.pyc deleted file mode 100644 index 39b2832..0000000 Binary files a/CFAPyX/__pycache__/backendentrypoint.cpython-311.pyc and /dev/null differ diff --git a/CFAPyX/__pycache__/datastore.cpython-311.pyc b/CFAPyX/__pycache__/datastore.cpython-311.pyc deleted file mode 100644 index 7decd8e..0000000 Binary files a/CFAPyX/__pycache__/datastore.cpython-311.pyc and /dev/null differ diff --git a/CFAPyX/__pycache__/decoder.cpython-311.pyc b/CFAPyX/__pycache__/decoder.cpython-311.pyc deleted file mode 100644 index 970038b..0000000 Binary files a/CFAPyX/__pycache__/decoder.cpython-311.pyc and /dev/null differ diff --git a/CFAPyX/__pycache__/fragmentarray.cpython-311.pyc b/CFAPyX/__pycache__/fragmentarray.cpython-311.pyc deleted file mode 100644 index 3cc7c9b..0000000 Binary files a/CFAPyX/__pycache__/fragmentarray.cpython-311.pyc and /dev/null differ diff --git a/CFAPyX/__pycache__/utils.cpython-311.pyc b/CFAPyX/__pycache__/utils.cpython-311.pyc deleted file mode 100644 index d3d443e..0000000 Binary files a/CFAPyX/__pycache__/utils.cpython-311.pyc and /dev/null differ diff --git a/CFAPyX/active.py b/CFAPyX/active.py deleted file mode 100644 index 014ea49..0000000 --- a/CFAPyX/active.py +++ /dev/null @@ -1,30 +0,0 @@ -import dask.array as da - -class CFAActiveArray(): - - description = "CFA wrapper to the xarray.Dataset dask array, enabling the use of Active Storage." - - # Note this implementation is currently ignored by xarray, which may just extract the `array` element? - - # __getitem__ is called which means the dask array is just a container, the actual method operations - # take place elsewhere - - def __init__( - self, - dsk, - name, - chunks=None, - dtype=None, - **kwargs - ): - - self._array = da.Array(dsk, name, chunks=chunks, dtype=dtype, **kwargs) - - def __getattr__(self, attr): - return getattr(self._array, attr) - - def __getitem__(self, item): - return self._array[item] - - def mean(self, **kwargs): - return self._array.mean(**kwargs) \ No newline at end of file diff --git a/CFAPyX/backendentrypoint.py b/CFAPyX/backend.py similarity index 86% rename from CFAPyX/backendentrypoint.py rename to CFAPyX/backend.py index 090df6a..66edd9d 100644 --- a/CFAPyX/backendentrypoint.py +++ b/CFAPyX/backend.py @@ -1,3 +1,6 @@ +__author__ = "Daniel Westwood" +__contact__ = "daniel.westwood@stfc.ac.uk" +__copyright__ = "Copyright 2023 United Kingdom Research and Innovation" from xarray.backends import StoreBackendEntrypoint, BackendEntrypoint from xarray.backends.common import AbstractDataStore @@ -7,7 +10,6 @@ from CFAPyX.datastore import CFADataStore from importlib.metadata import entry_points -#engine = entry_points(group='xarray.backends') def open_cfa_dataset( filename_or_obj, @@ -19,6 +21,7 @@ def open_cfa_dataset( use_cftime=None, decode_timedelta=None, cfa_options={}, + active_options={}, group=None, ): """ @@ -46,7 +49,8 @@ def open_cfa_dataset( store = CFADataStore.open(filename_or_obj, group=group) # Expands cfa_options into individual kwargs for the store. - store.cfa_options = cfa_options + store.cfa_options = cfa_options + store.active_options = active_options # Xarray makes use of StoreBackendEntrypoints to provide the Dataset 'ds' store_entrypoint = CFAStoreBackendEntrypoint() @@ -59,6 +63,7 @@ def open_cfa_dataset( drop_variables=drop_variables, use_cftime=use_cftime, decode_timedelta=decode_timedelta, + use_active=store.use_active ) return ds @@ -80,6 +85,7 @@ def open_dataset( use_cftime=None, decode_timedelta=None, cfa_options={}, + active_options={}, group=None, # backend specific keyword arguments # do not use 'chunks' or 'cache' here @@ -99,12 +105,12 @@ def open_dataset( use_cftime=use_cftime, decode_timedelta=decode_timedelta, cfa_options=cfa_options, + active_options=active_options, group=group) - class CFAStoreBackendEntrypoint(StoreBackendEntrypoint): description = "Open CFA-based Abstract Data Store" - url = "https://docs.xarray.dev/en/stable/generated/xarray.backends.StoreBackendEntrypoint.html" + url = "https://cedadev.github.io/CFAPyX/" def open_dataset( self, @@ -117,6 +123,7 @@ def open_dataset( drop_variables=None, use_cftime=None, decode_timedelta=None, + use_active=False, ) -> Dataset: """ Takes cfa_xarray_store of type AbstractDataStore and creates an xarray.Dataset object. @@ -128,7 +135,6 @@ def open_dataset( :returns: An xarray.Dataset object composed of xarray.DataArray objects representing the different NetCDF variables and dimensions. CFA aggregated variables are decoded unless the ``decode_cfa`` parameter in ``cfa_options`` is false. - """ assert isinstance(cfa_xarray_store, AbstractDataStore) @@ -151,7 +157,18 @@ def open_dataset( ) # Create the xarray.Dataset object here. - ds = Dataset(vars, attrs=attrs) + if use_active: + try: + from XarrayActive import ActiveDataset + + ds = ActiveDataset(vars, attrs=attrs) + except ImportError: + raise ImportError( + '"ActiveDataset" from XarrayActive failed to import - please ensure you have the XarrayActive package installed.' + ) + else: + ds = Dataset(vars, attrs=attrs) + ds = ds.set_coords(coord_names.intersection(vars)) ds.set_close(cfa_xarray_store.close) ds.encoding = encoding diff --git a/CFAPyX/datastore.py b/CFAPyX/datastore.py index 58b1237..027eac1 100644 --- a/CFAPyX/datastore.py +++ b/CFAPyX/datastore.py @@ -1,3 +1,7 @@ +__author__ = "Daniel Westwood" +__contact__ = "daniel.westwood@stfc.ac.uk" +__copyright__ = "Copyright 2023 United Kingdom Research and Innovation" + from xarray.backends import ( NetCDF4DataStore ) @@ -14,12 +18,12 @@ import netCDF4 import numpy as np import os +import re -from CFAPyX.utils import _ensure_fill_value_valid -from CFAPyX.fragmentarray import FragmentArrayWrapper -from CFAPyX.decoder import chunk_locations, chunk_positions +from CFAPyX.wrappers import FragmentArrayWrapper +from CFAPyX.decoder import get_fragment_positions, get_fragment_extents -from CFAPyX.group import GroupedDatasetWrapper +from CFAPyX.group import CFAGroupWrapper xarray_subs = { @@ -27,62 +31,296 @@ } class CFADataStore(NetCDF4DataStore): + """ - DataStore container for the CFA-netCDF loaded file. Contains all unpacking routines directly - related to the specific variables and attributes, but uses CFAPyX.utils for some of the aggregation - metadata decoding. + DataStore container for the CFA-netCDF loaded file. Contains all unpacking routines + directly related to the specific variables and attributes. The ``NetCDF4Datastore`` + Xarray class from which this class inherits, has an ``__init__`` method which + cannot easily be overriden, so properties are used instead for specific variables + that may be un-set at time of use. """ + @property + def active_options(self): + """ + Property of the datastore that relates private option variables to the standard + ``active_options`` parameter. + """ + return { + 'use_active': self.use_active, + 'chunks': self._active_chunks, + } + + @active_options.setter + def active_options(self, value): + self._set_active_options(**value) + + def _set_active_options(self, use_active=False, chunks=None): + self.use_active = use_active + self._active_chunks = chunks + + @property + def _active_chunks(self): + if hasattr(self,'__active_chunks'): + return self.__active_chunks + return None + + @_active_chunks.setter + def _active_chunks(self, value): + self.__active_chunks = value + + @property + def cfa_options(self): + """ + Property of the datastore that relates private option variables to the standard + ``cfa_options`` parameter. + """ + + return { + 'substitutions': self._substitutions, + 'decode_cfa': self._decode_cfa + } + + @cfa_options.setter + def cfa_options(self, value): + self._set_cfa_options(**value) + + def _set_cfa_options( + self, + substitutions=None, + decode_cfa=True + ): + """ + Method to set cfa options. + + :param substitutions: (dict) Set of provided substitutions to Xarray, + following the CFA conventions on substitutions. + + :param decode_cfa: (bool) Optional setting to disable CFA decoding + in some cases, default is True. + """ + + self._substitutions = substitutions + self._decode_cfa = decode_cfa + def _acquire(self, needs_lock=True): """ Fetch the global or group dataset from the Datastore Caching Manager (NetCDF4) """ with self._manager.acquire_context(needs_lock) as root: - ds = GroupedDatasetWrapper.open(root, self._group, self._mode) + ds = CFAGroupWrapper.open(root, self._group, self._mode) + + self.conventions = ds.Conventions return ds - def get_variables(self): + def _decode_feature_data(self, feature_data, readd={}): + """ + Decode the value of an object which is expected to be of the form of a + ``feature: variable`` blank-separated element list. + """ + parts = re.split(': | ',feature_data) + + # Anything that uses a ':' needs to be readded after the previous step. + for k, v in readd: + for p in parts: + p.replace(k,v) + + return {k: v for k, v in zip(parts[0::2], parts[1::2])} + + def _check_applied_conventions(self, agg_data): + """ + Check that the aggregated data complies with the conventions specified in the + CFA-netCDF file + """ + + required = ('shape', 'location', 'address') + if 'CFA-0.6.2' in self.conventions.split(' '): + required = ('location', 'file', 'format') + + for feature in required: + if feature not in agg_data: + raise ValueError( + f'CFA-netCDF file is not compliant with {self.conventions} ' + f'Required aggregated data features: "{required}", ' + f'Received "{tuple(agg_data.keys())}"' + ) + + def _perform_decoding( + self, + shape, + address, + location, + array_shape, + value=None, + cformat='', + substitutions=None): + """ + Private method for performing the decoding of the standard ``fragment array + variables``. Any convention version-specific adjustments should be made prior + to decoding with this function, namely in the public method of the same name. + + :param shape: (obj) The integer-valued ``shape`` fragment array variable + defines the shape of each fragment's data in its canonical + form. CF-1.12 section 2.8.1 + + :param address: (obj) The ``address`` fragment array variable, that may + have any data type, defines how to find each fragment + within its fragment dataset. CF-1.12 section 2.8.1 + + :param location: (obj) The string-valued ``location`` fragment array + variable defines the locations of fragment datasets using + Uniform Resource Identifiers (URIs). CF-1.12 section 2.8.1 + + :param value: (obj) *Optional* unique data value to fill a fragment array + where the data values within the fragment are all the same. + + :param cformat: (str) *Optional* ``format`` argument if provided by the + CFA-netCDF or cfa-options parameters. CFA-0.6.2 + + :param substitutions: + + :returns: (fragment_info) A dictionary of fragment metadata where each + key is the coordinates of a fragment in index space and the + value is a dictionary of the attributes specific to that + fragment. + """ - Fetch the netCDF4.Dataset variables and perform some CFA decoding if necessary. - .. Note:: - ``ds`` is now a ``GroupedDatasetWrapper`` object from ``CFAPyX.group`` which has flattened - the group structure and allows fetching of variables and attributes from the whole group tree - from which a specific group may inherit. + fragment_info = {} + + # Extract non-padded fragment sizes per dimension. + fragment_size_per_dim = [i.compressed().tolist() for i in shape] + + # Derive the total shape of the fragment array in all fragmented dimensions. + fragment_space = [len(fsize) for fsize in fragment_size_per_dim] + + # Obtain the positions of each fragment in index space. + fragment_positions = get_fragment_positions(fragment_size_per_dim) + + global_extent, extent, shapes = get_fragment_extents( + fragment_size_per_dim, + array_shape + ) + + if value is not None: + # -------------------------------------------------------- + # This fragment contains a constant value, not file + # locations. + # -------------------------------------------------------- + fragment_space = value.shape + fragment_info = { + frag_pos: { + "shape": shapes[frag_pos], + "fill_value": value[frag_pos].item(), + "global_extent": global_extent[frag_pos], + "extent": extent[frag_pos], + "format": "full", + } + for frag_pos in fragment_positions + } + + return fragment_info, fragment_space + + constructor_shape = location.shape + + if not address.ndim: # Scalar address + addr = address.getValue() + adtype = np.array(addr).dtype + address = np.full(constructor_shape, addr, dtype=adtype) + + if cformat != '': + if not cformat.ndim: + cft = cformat.getValue() + npdtype = np.array(cft).dtype + cformat = np.full(constructor_shape, cft, dtype=npdtype) + + for frag_pos in fragment_positions: + + fragment_info[frag_pos] = { + "shape" : shapes[frag_pos], + "location" : location[frag_pos], + "address" : address[frag_pos], + "extent" : extent[frag_pos], + "global_extent": global_extent[frag_pos] + } + if hasattr(cformat, 'shape'): + fragment_info[frag_pos]["format"] = cformat[frag_pos] - :returns: A ``FrozenDict`` Xarray object of the names of all variables, and methods to fetch those - variables, depending on if those variables are standard NetCDF4 or CFA Aggregated variables. + # Apply string substitutions to the fragment filenames + if substitutions: + for value in fragment_info.values(): + for base, sub in substitutions.items(): + value["location"] = value["location"].replace(base, sub) + + return fragment_info, fragment_space + + # Public class methods + + def perform_decoding(self, array_shape, agg_data): + """ + Public method ``perform_decoding`` involves extracting the aggregated + information parameters and assembling the required information for actual + decoding. """ - xarray_vars = {} - r = {} # Real size of dimensions for aggregated variables. + # If not raised an error in checking, we can continue. + self._check_applied_conventions(agg_data) - if not self.decode_cfa: - return FrozenDict( - (k, self.open_variable(k, v, r)) for k, v in self.ds.variables.items() + cformat = '' + value = None + try: + if 'CFA-0.6.2' in self.conventions: + shape = self.ds.variables[agg_data['location']] + location = self.ds.variables[agg_data['file']] + cformat = self.ds.variables[agg_data['format']] + else: # Default to CF-1.12 + shape = self.ds.variables[agg_data['shape']] + location = self.ds.variables[agg_data['location']] + if 'value' in agg_data: + value = self.ds.variables[agg_data['value']] + + address = self.ds.variables[agg_data['address']] + except: + raise ValueError( + 'One or more aggregated data features specified could not be ' + 'found in the data: ' + f'"{tuple(agg_data.keys())}"' ) + + subs = {} + if hasattr(location, 'substitutions'): + subs = location.substitutions.replace('https://', 'https@//') + subs = self._decode_feature_data(subs, readd={'https://':'https@//'}) - ## Proceed with decoding CFA content. + return self._perform_decoding(shape, address, location, array_shape, + cformat=cformat, value=value, + substitutions = xarray_subs | subs) + # Combine substitutions with known defaults for using in xarray. - if not hasattr(self, '_decoded_cfa'): - self.perform_decoding() + def get_variables(self): + """ + Fetch the netCDF4.Dataset variables and perform some CFA decoding if + necessary. - standardised_terms = ( - "cfa_location", - "cfa_file", - "cfa_address", - "cfa_format" - ) + ``ds`` is now a ``GroupedDatasetWrapper`` object from ``CFAPyX.group`` which + has flattened the group structure and allows fetching of variables and + attributes from the whole group tree from which a specific group may inherit. - ## Decide which dimensions and variables can be ignored when constructing the CFA Dataset. + :returns: A ``FrozenDict`` Xarray object of the names of all variables, + and methods to fetch those variables, depending on if those + variables are standard NetCDF4 or CFA Aggregated variables. + """ + + if not self._decode_cfa: + return FrozenDict( + (k, self.open_variable(k, v)) for k, v in self.ds.variables.items() + ) + # Determine CFA-aggregated variables + all_vars, real_vars = {}, {} - ## Obtain the list of fragmented dimensions and their real sizes. - for dimension in self.ds.dimensions.keys(): - if 'f_' in dimension and '_loc' not in dimension: - real_dim = dimension.replace('f_','') - r[real_dim] = self.ds.dimensions[real_dim].size + fragment_array_vars = [] ## Ignore variables in the set of standardised terms. for avar in self.ds.variables.keys(): @@ -91,105 +329,87 @@ def get_variables(self): if hasattr(self.ds.variables[avar], 'aggregated_dimensions'): cfa = True - if avar not in standardised_terms: - xarray_vars[avar] = (self.ds.variables[avar], cfa) + agg_data = self.ds.variables[avar].aggregated_data.split(' ') + + for vname in agg_data: + fragment_array_vars += re.split(': | ',vname) + + all_vars[avar] = (self.ds.variables[avar], cfa) + + # Ignore fragment array variables at this stage of decoding. + for var in all_vars.keys(): + if var not in fragment_array_vars: + real_vars[var] = all_vars[var] + return FrozenDict( - (k, self.open_variable(k, v, r)) for k, v in xarray_vars.items() + (k, self.open_variable(k, v)) for k, v in real_vars.items() ) def get_attrs(self): """ - Produce the FrozenDict of attributes from the ``NetCDF4.Dataset`` or ``GroupedDatasetWrapper`` in - the case of using a group or nested group tree. + Produce the FrozenDict of attributes from the ``NetCDF4.Dataset`` or + ``CFAGroupWrapper`` in the case of using a group or nested group tree. """ return FrozenDict((k, self.ds.getncattr(k)) for k in self.ds.ncattrs()) - @property - def cfa_options(self): - """Property of the datastore that relates private option variables to the standard ``cfa_options`` parameter.""" - return { - 'substitutions': self._substitutions, - 'decode_cfa': self._decode_cfa - } - - @cfa_options.setter - def cfa_options(self, value): - self._set_cfa_options(**value) - - def _set_cfa_options( - self, - substitutions=None, - decode_cfa=True - ): - """ - Method to set cfa options. - - :param substitutions: (dict) Set of provided substitutions to Xarray, following the CFA - conventions on substitutions. - - :param decode_cfa: (bool) Optional setting to disable CFA decoding in some cases, default - is True. - """ - - self._substitutions = substitutions - self._decode_cfa = decode_cfa - - @property - def decode_cfa(self): - return self._decode_cfa - - def open_variable(self, name: str, var, real_agg_dims): + def open_variable(self, name: str, var): """ - Open a CFA-netCDF variable as either a standard NetCDF4 Datastore variable or as a - CFA aggregated variable which requires additional decoding. - - :param name: (str) A named NetCDF4 variable. + Open a CFA-netCDF variable as either a standard NetCDF4 Datastore variable + or as a CFA aggregated variable which requires additional decoding. - :param var: (obj) The NetCDF4.Variable object or a tuple with the contents - ``(NetCDF4.Variable, cfa)`` where ``cfa`` is a bool that determines - if the variable is a CFA or standard variable. + :param name: (str) A named NetCDF4 variable. - :param real_agg_dims: (dict) Named fragment dimensions with their corresponding sizes in - array space. + :param var: (obj) The NetCDF4.Variable object or a tuple with the contents + ``(NetCDF4.Variable, cfa)`` where ``cfa`` is a bool that + determines if the variable is a CFA or standard variable. - :returns: The variable object opened as either a standard store variable or CFA aggregated variable. + :returns: The variable object opened as either a standard store variable + or CFA aggregated variable. """ if type(var) == tuple: - if var[1] and self.decode_cfa: - variable = self.open_cfa_variable(name, var[0], real_agg_dims) + if var[1] and self._decode_cfa: + variable = self.open_cfa_variable(name, var[0]) else: variable = self.open_store_variable(name, var[0]) else: variable = self.open_store_variable(name, var) return variable - def open_cfa_variable(self, name: str, var, real_agg_dims): + def open_cfa_variable(self, name: str, var): """ - Open a CFA Aggregated variable with the correct parameters to create an Xarray ``Variable`` instance. + Open a CFA Aggregated variable with the correct parameters to create an + Xarray ``Variable`` instance. :param name: (str) A named NetCDF4 variable. - :param var: (obj) The NetCDF4.Variable object or a tuple with the contents - ``(NetCDF4.Variable, cfa)`` where ``cfa`` is a bool that determines - if the variable is a CFA or standard variable. + :param var: (obj) The NetCDF4.Variable object or a tuple with the + contents ``(NetCDF4.Variable, cfa)`` where ``cfa`` is + a bool that determines if the variable is a CFA or + standard variable. - :param real_agg_dims: (dict) Named fragment dimensions with their corresponding sizes in - array space. - - :returns: An xarray ``Variable`` instance constructed from the attributes provided here, and - data provided by a ``FragmentArrayWrapper`` which is indexed by Xarray's ``LazilyIndexedArray`` class. + :returns: An xarray ``Variable`` instance constructed from the + attributes provided here, and data provided by a + ``FragmentArrayWrapper`` which is indexed by Xarray's + ``LazilyIndexedArray`` class. """ + real_dims = { + d: self.ds.dimensions[d].size for d in var.aggregated_dimensions.split(' ') + } + agg_data = self._decode_feature_data(var.aggregated_data) + ## Array Metadata - dimensions = tuple(real_agg_dims.keys()) - ndim = len(dimensions) - array_shape = tuple(real_agg_dims.values()) + dimensions = tuple(real_dims.keys()) + array_shape = tuple(real_dims.values()) + fragment_info, fragment_space = self.perform_decoding(array_shape, agg_data) + + units = '' if hasattr(var, 'units'): units = getattr(var, 'units') - else: - units = '' + if hasattr(var, 'aggregated_units'): + units = getattr(var, 'aggregated_units') ## Get non-aggregated attributes. attributes = {} @@ -200,12 +420,14 @@ def open_cfa_variable(self, name: str, var, real_agg_dims): ## Array-like object data = indexing.LazilyIndexedArray( FragmentArrayWrapper( - self._decoded_cfa, - ndim=ndim, + fragment_info, + fragment_space, shape=array_shape, units=units, dtype=var.dtype, cfa_options=self.cfa_options, + active_options=self.active_options, + named_dims=dimensions, )) encoding = {} @@ -219,8 +441,10 @@ def open_cfa_variable(self, name: str, var, real_agg_dims): ) else: encoding["dtype"] = var.dtype - _ensure_fill_value_valid(data, attributes) - # netCDF4 specific encoding; save _FillValue for later + + if data.dtype.kind == "S" and "_FillValue" in attributes: + attributes["_FillValue"] = np.bytes_(attributes["_FillValue"]) + filters = var.filters() if filters is not None: encoding.update(filters) @@ -243,157 +467,4 @@ def open_cfa_variable(self, name: str, var, real_agg_dims): v = Variable(dimensions, data, attributes, encoding) return v - - def _get_xarray_fragment(self, filename, address, dtype, units, shape): - dsF = xarray.open_dataset(filename) - fragment = dsF[address] - assert fragment.shape == shape - assert fragment.dtype == dtype - - if hasattr(fragment, 'units'): - assert fragment.units == units - elif units != None: - print("Warning: Fragment does not contain units, while units were expected") - - return fragment - - def _perform_decoding(self, location, address, file, cformat, term, substitutions=None): - aggregated_data = {} - - ndim = location.shape[0] - - chunks = [i.compressed().tolist() for i in location] - shape = [sum(c) for c in chunks] - positions = chunk_positions(chunks) - locations = chunk_locations(chunks) - - if term is not None: - # -------------------------------------------------------- - # This fragment contains a constant value, not file - # locations. - # -------------------------------------------------------- - term = str(term) - fragment_shape = term.shape - aggregated_data = { - frag_loc: { - "location": loc, - "fill_value": term[frag_loc].item(), - "format": "full", - } - for frag_loc, loc in zip(positions, locations) - } - else: - - extra_dimension = file.ndim > ndim - if extra_dimension: - # There is an extra non-fragment dimension - fragment_shape = file.shape[:-1] - else: - fragment_shape = file.shape - - #print(f.shape, a.getValue(), a.dtype) - - if not address.ndim: - addr = address.getValue() - adtype = np.array(addr).dtype - address = np.full(fragment_shape, addr, dtype=adtype) - - if not cformat.ndim: - # Properly convert into numpy types - cft = cformat.getValue() - npdtype = np.array(cft).dtype - cformat = np.full(fragment_shape, cft, dtype=npdtype) - - if extra_dimension: - aggregated_data = { - frag_loc: { - "location": loc, - "filename": file[frag_loc].tolist(), - "address": address[frag_loc].tolist(), - "format": cformat[frag_loc].item(), - } - for frag_loc, loc in zip(positions, locations) - } - else: - aggregated_data = { - frag_loc: { - "location": loc, - "filename": file[frag_loc], - "address": address[frag_loc], - "format": cformat[frag_loc], - } - for frag_loc, loc in zip(positions, locations) - } - - # Apply string substitutions to the fragment filenames - if substitutions: - for value in aggregated_data.values(): - for base, sub in substitutions.items(): - value["filename"] = value["filename"].replace(base, sub) - - return fragment_shape, aggregated_data - - def perform_decoding(self): - - try: - location = self.ds.variables['cfa_location'] - file = self.ds.variables['cfa_file'] - address = self.ds.variables['cfa_address'] - cformat = self.ds.variables['cfa_format'] - except: - raise ValueError( - "Unable to locate CFA Decoding instructions" - ) - - fragment_shape, aggregated_data = self._perform_decoding(location, address, file, cformat, term=None, substitutions=xarray_subs) - - self._decoded_cfa = { - 'fragment_shape': fragment_shape, - 'aggregated_data': aggregated_data - } - - def test_load(self): - - param1 = self.ds.ncattrs() - param2 = self.ds.variables - - ## CFA Instruction Variables - - # Location is the most complicated to deal with - must be expanded. - location = self.ds.variables['cfa_location'] - file = self.ds.variables['cfa_file'] - address = self.ds.variables['cfa_address'] - cformat = self.ds.variables['cfa_format'] - - ## Aggregated Variables - #aggregated_vars = {avar: self.ds.variables[avar] for avar in self.ds.dimensions.keys() if hasattr(self.ds.variables[avar], 'aggregated_dimensions')} - - - ## Aggregation Dimensions - # Important aggregation dimensions start with 'f_' - assumption! - #cfa_dims = {cfd: self.ds.dimensions[cfd] for cfd in self.ds.dimensions.keys() if 'f_' in cfd} - std_dims = [d for d in self.ds.dimensions.keys() if 'f_' not in d] - - fragment_shape, aggregated_data = self._perform_decoding(location, address, file, cformat, None, substitutions=xarray_subs) - - fragments = [] - # Recheck how cf-python does the decoding. - - concat_dims = [std_dims[i] for i in range(len(fragment_shape)) if fragment_shape[i] > 1] - - for fragment in aggregated_data.keys(): - finfo = aggregated_data[fragment] - arr_fragment = self._get_xarray_fragment( - filename=finfo['filename'], - address=finfo['address'], - dtype=np.dtype(np.float64), # from aggregated vars - shape=(2,180,360), # from aggregated vars - units=None, # from aggregated vars - ) - - # Open all fragments as xarray sections and combine into a single data array - fragments.append(arr_fragment) - - agg_ds = xarray.combine_nested(fragments, concat_dims) - - return None + diff --git a/CFAPyX/decoder.py b/CFAPyX/decoder.py index 5df8543..50eb844 100644 --- a/CFAPyX/decoder.py +++ b/CFAPyX/decoder.py @@ -1,198 +1,183 @@ +__author__ = "Daniel Westwood" +__contact__ = "daniel.westwood@stfc.ac.uk" +__copyright__ = "Copyright 2023 United Kingdom Research and Innovation" + from itertools import accumulate, product from dask.array.core import normalize_chunks -def chunk_positions(chunks): - """ - Determine the position of each chunk. Copied directly from cf-python, version 3.14.0 onwards. - +def get_fragment_positions(fragment_size_per_dim): """ - return product(*(range(len(bds)) for bds in chunks)) + Get the positions in index space for each fragment. -def chunk_locations(chunks): - """Determine the shape of each chunk. Copied directly from cf-python, version 3.15.0 onwards. + :param fragment_size_per_dim: (list) The set of fragment sizes per dimension. first dimension has length + equal to the number of array dimensions, second dimension is a list of the + fragment sizes for the corresponding array dimension. - :param chunks: (tuple) The chunk sizes along each dimension, as output by - `dask.array.Array.chunks`. + :returns: A list of tuples representing the positions of all the fragments in index space given by the + fragment_size_per_dim. - :returns: The location/shape of each array chunk within the corresponding fragment file. """ - from dask.utils import cached_cumsum + return product(*(range(len(sizes)) for sizes in fragment_size_per_dim)) - cumdims = [cached_cumsum(bds, initial_zero=True) for bds in chunks] - locations = [ - [(s, s + dim) for s, dim in zip(starts, shapes)] - for starts, shapes in zip(cumdims, chunks) - ] - return product(*locations) - -def fragment_descriptors(fsizes_per_dim, fragment_dims, array_shape): +def get_fragment_extents(fragment_size_per_dim, array_shape): """ Return descriptors for every fragment. Copied from cf-python version 3.14.0 onwards. - :param fsizes_per_dim: (tuple) Size of the fragment array in each dimension. - - :param fragment_dims: (tuple) The indexes of dimensions which are fragmented. + :param fragment_size_per_dim: (list) The set of fragment sizes per dimension. first dimension has length + equal to the number of array dimensions, second dimension is a list of the + fragment sizes for the corresponding array dimension. - :param array_shape: (tuple) The shape of the total array. + :param array_shape: (tuple) The shape of the total array in ``array space``. :returns: - 6-`tuple` of iterators - Each iterator iterates over a particular descriptor - from each subarray. - - 1. The indices of the aggregated array that correspond - to each subarray. + global_extents - The array of slice objects for each fragment which define where the fragment + slots into the total array. - 2. The shape of each subarray. + extents - The extents to be applied to each fragment, usually just the whole fragment array. - 3. The indices of the fragment that corresponds to each - subarray (some subarrays may be represented by a - part of a fragment). + shapes - The shape of each fragment in ``array space``. - 4. The location of each subarray. + """ - 5. The location on the fragment dimensions of the - fragment that corresponds to each subarray. + ndim = len(fragment_size_per_dim) - 6. The shape of each fragment that overlaps each chunk. + initial = [0 for i in range(ndim)] + final = [len(i) for i in fragment_size_per_dim] - """ + fragmented_dims = [i for i in range(len(fragment_size_per_dim)) if len(fragment_size_per_dim[i]) != 1] - # The indices of the uncompressed array that correspond to - # each subarray, the shape of each uncompressed subarray, and - # the location of each subarray - s_locations = [] - u_shapes = [] - u_indices = [] + dim_shapes = [] + dim_indices = [] f_locations = [] - for dim, fs in enumerate(fsizes_per_dim): - num_fs = len(fs) - # (0, 1, 2, 3 ... num_fragments) in each dimension - s_locations.append(tuple(range(num_fs))) + for dim, fs in enumerate(fragment_size_per_dim): + # (num_fragments) for each dimension - u_shapes.append(fs) + dim_shapes.append(fs) + + fsa = tuple(accumulate((0,) + tuple(fs))) - if dim in fragment_dims: + if dim in fragmented_dims: # Same as s_locations - f_locations.append(tuple(range(num_fs))) + f_locations.append(tuple(range(len(fs)))) else: # No fragmentation along this dimension # (0, 0, 0, 0 ...) in each non-fragmented dimension. - f_locations.append((0,) * num_fs) + f_locations.append((0,) * len(fs)) - fs = tuple(accumulate((0,) + fs)) - u_indices.append([slice(i, j) for i, j in zip(fs[:-1], fs[1:])]) + # List of slices a to a+1 for every item in the new fs. + dim_indices.append([slice(i, j) for i, j in zip(fsa[:-1], fsa[1:])]) - # For each fragment, we define a slice that corresponds - # to the data we want to collect from it. - f_indices = [ - (slice(None),) * len(u) if dim in fragment_dims else u - for dim, u in enumerate(u_indices) + # Transform f_locations to get a dict of positions with a slice and shape for each. + positions = [ + coord for coord in product( + *[range(r[0], r[1]) for r in zip(initial, final)] + ) ] - # For each fragment, we define a shape that corresponds to - # the sliced data. + f_indices = [] + for dim, u in enumerate(dim_indices): + if dim in fragmented_dims: + f_indices.append( (slice(None),) * len(u)) + else: + f_indices.append( u ) + f_shapes = [ - u_shape if dim in fragment_dims else (size,) * len(u_shape) - for dim, (u_shape, size) in enumerate(zip(u_shapes, array_shape)) + dim_shape if dim in fragmented_dims else (size,) * len(dim_shape) + for dim, (dim_shape, size) in enumerate(zip(dim_shapes, array_shape)) ] - return ( - product(*u_indices), - product(*u_shapes), - product(*f_indices), - product(*s_locations), - product(*f_locations), - product(*f_shapes), - ) - -def fragment_shapes(shapes, array_shape, fragment_dims, fragment_shape, aggregated_data, ndim, dtype): + global_extents = {} + extents = {} + shapes = {} + + for frag_pos in positions: + extents[frag_pos] = [] + global_extents[frag_pos] = [] + shapes[frag_pos] = [] + for a, i in enumerate(frag_pos): + extents[frag_pos].append( + f_indices[a][i] + ) + global_extents[frag_pos].append( + dim_indices[a][i] + ) + shapes[frag_pos].append( + f_shapes[a][i] + ) + + return global_extents, extents, shapes + +def get_dask_chunks( + array_space, + fragment_space, + extent, + dtype, + explicit_shapes=None): """ - Create what is later referred to as 'chunks'. Copied from cf-python version 3.14.0 onwards. - - **Requires updating**. - - :Parameters: - - shapes: `int`, sequence, `dict` or `str`, optional - Define the chunk shapes. + Define the `chunks` array passed to Dask when creating a Dask Array. This is an array of fragment sizes + per dimension for each of the relevant dimensions. Copied from cf-python version 3.14.0 onwards. - Any value accepted by the *chunks* parameter of the - `dask.array.from_array` function is allowed. + :param array_space: (tuple) The shape of the array in ``array space``. - The chunk sizes implied by *chunks* for a dimension - that has been fragmented are ignored, so their - specification is arbitrary. + :param fragment_space: (tuple) The shape of the array in ``fragment space``. - array_shape: + :param extent: (dict) The global extent of each fragment - where it fits into the total array for this variable (in array space). - fragment_dims: + :param dtype: (obj) The datatype for this variable. - fragment_shape: + :param explicit_shapes: (tuple) Set of shapes to apply to the fragments - currently not implemented outside this function. - aggregated_data: - - ndim: - - dtype: - - :Returns: - - `tuple` - The chunk sizes along each dimension. + :returns: A tuple of the chunk sizes along each dimension. """ from numbers import Number - from dask.array.core import normalize_chunks - # Create the base chunks. - fsizes_per_dim = [] + ndim = len(array_space) + fsizes_per_dim, fragmented_dim_indices = [],[] + + for dim, n_fragments in enumerate(fragment_space): + if n_fragments != 1: - for dim, (n_fragments, size) in enumerate( - zip(fragment_shape, array_shape) - ): - if dim in fragment_dims: - # This aggregated dimension is spanned by more than - # one fragment. - fs = [] + fsizes = [] index = [0] * ndim for n in range(n_fragments): index[dim] = n - loc = aggregated_data[tuple(index)]["location"][dim] # Update for CF-1.12 - fragment_size = loc[1] - loc[0] - fs.append(fragment_size) + ext = extent[tuple(index)][dim] + fragment_size = ext.stop - ext.start + fsizes.append(fragment_size) - fsizes_per_dim.append(tuple(fs)) + fsizes_per_dim.append(tuple(fsizes)) + fragmented_dim_indices.append(dim) else: # This aggregated dimension is spanned by exactly one # fragment. Store None, for now, in the expectation - # that it will get overwrittten. + # that it will get overwritten. fsizes_per_dim.append(None) - ## Handle custom shapes for the fragments. + ## Handle explicit shapes for the fragments. - if isinstance(shapes, (str, Number)) or shapes is None: - fsizes_per_dim = [ # For each dimension, use fs or shapes if the dimension is fragmented or not respectively. - fs if i in fragment_dims else shapes for i, fs in enumerate(fsizes_per_dim) + if isinstance(explicit_shapes, (str, Number)) or explicit_shapes is None: + fsizes_per_dim = [ # For each dimension, use fs or explicit_shapes if the dimension is fragmented or not respectively. + fs if i in fragmented_dim_indices else explicit_shapes for i, fs in enumerate(fsizes_per_dim) ] - elif isinstance(shapes, dict): + elif isinstance(explicit_shapes, dict): fsizes_per_dim = [ - fsizes_per_dim[i] if i in fragment_dims else shapes.get(i, "auto") + fsizes_per_dim[i] if i in fragmented_dim_indices else explicit_shapes.get(i, "auto") for i, fs in enumerate(fsizes_per_dim) ] else: - # Shapes is a sequence - if len(shapes) != ndim: + # explicit_shapes is a sequence + if len(explicit_shapes) != ndim: raise ValueError( - f"Wrong number of 'shapes' elements in {shapes}: " - f"Got {len(shapes)}, expected {ndim}" + f"Wrong number of 'explicit_shapes' elements in {explicit_shapes}: " + f"Got {len(explicit_shapes)}, expected {ndim}" ) fsizes_per_dim = [ - fs if i in fragment_dims else shapes[i] for i, fs in enumerate(fsizes_per_dim) + fs if i in fragmented_dim_indices else explicit_shapes[i] for i, fs in enumerate(fsizes_per_dim) ] - return normalize_chunks(fsizes_per_dim, shape=array_shape, dtype=dtype) \ No newline at end of file + return normalize_chunks(fsizes_per_dim, shape=array_space, dtype=dtype) \ No newline at end of file diff --git a/CFAPyX/fragmentarray.py b/CFAPyX/fragmentarray.py deleted file mode 100644 index e2b7584..0000000 --- a/CFAPyX/fragmentarray.py +++ /dev/null @@ -1,276 +0,0 @@ -from CFAPyX.utils import OneOrMoreList -from CFAPyX.decoder import fragment_shapes, fragment_descriptors -from CFAPyX.active import CFAActiveArray - -import dask.array as da -from dask.array.core import getter -from dask.base import tokenize -from dask.utils import SerializableLock - -from itertools import product - -import netCDF4 - -import numpy as np - -class FragmentArrayWrapper(): - - def __init__(self, decoded_cfa, ndim, shape, units, dtype, cfa_options=None): - - self.aggregated_data = decoded_cfa['aggregated_data'] - self.fragment_shape = decoded_cfa['fragment_shape'] - - fragments = list(self.aggregated_data.keys()) - - # Required parameters list - self.ndim = ndim - self.shape = shape - self.units = units - self.dtype = dtype - - self.fragment_dims = tuple([i for i in range(self.ndim) if self.fragment_shape[i] != 1]) - - if cfa_options: - self.cfa_options = cfa_options - self.apply_cfa_options(**cfa_options) - - self.__array_function__ = self.get_array - - def apply_cfa_options( - self, - substitutions=None, - decode_cfa=None - ): - - if substitutions: - - if type(substitutions) != list: - substitutions = [substitutions] - - for s in substitutions: - base, substitution = s.split(':') - for f in self.aggregated_data.keys(): - self.aggregated_data[f]['filename'] = self.aggregated_data[f]['filename'].replace(base, substitution) - - @property - def shape(self): - return self._shape - - @shape.setter - def shape(self, value): - self._shape = value - - @property - def units(self): - return self._units - - @units.setter - def units(self, value): - self._units = value - - @property - def dtype(self): - return self._dtype - - @dtype.setter - def dtype(self, value): - self._dtype = value - - @property - def ndim(self): - return self._ndim - - @ndim.setter - def ndim(self, value): - self._ndim = value - - def __getitem__(self, selection): - array = self.get_array() - return array[selection] - - def get_array(self, needs_lock=True): - - name = (f"{self.__class__.__name__}-{tokenize(self)}",) - - dtype = self.dtype - units = self.units - - calendar = None # Fix later - - aggregated_data = self.aggregated_data - - # For now expect to deal only with NetCDF Files - - fsizes_per_dim = fragment_shapes( - shapes = None, - array_shape = self.shape, - fragment_dims = self.fragment_dims, - fragment_shape = self.fragment_shape, - aggregated_data = aggregated_data, - ndim=self.ndim, - dtype=np.dtype(np.float64) - - ) - dsk = {} - - for ( - u_indices, - u_shape, - f_indices, - fragment_location, - fragment_location, - fragment_shape, - ) in zip(*fragment_descriptors(fsizes_per_dim, self.fragment_dims, self.shape)): - kwargs = aggregated_data[fragment_location].copy() - kwargs.pop("location",None) # Update for CF-1.12 - - fragment_format = kwargs.pop("format",None) - # Assume nc format for now. - - kwargs['fragment_location'] = fragment_location - kwargs['extent'] = None - - fragment = get_fragment_wrapper( - fragment_format, - dtype=dtype, - shape=fragment_shape, - aggregated_units=units, - aggregated_calendar=calendar, - **kwargs - ) - - key = f"{fragment.__class__.__name__}-{tokenize(fragment)}" - dsk[key] = fragment - dsk[name + fragment_location] = ( - getter, # From dask docs - key, - f_indices, - False, - getattr(fragment, "_lock", False) # Check version cf-python - ) - - return CFAActiveArray(dsk, name[0], chunks=fsizes_per_dim, dtype=dtype) - -def get_fragment_wrapper(format, **kwargs): - if format == 'nc': - return NetCDFFragmentWrapper(**kwargs) - else: - raise NotImplementedError( - f"Fragment type '{format}' not supported" - ) - -class FragmentWrapper(): - """ - Possible attributes to add: - - Units (special class) - - aggregated_Units - - size - - Possible Methods/Properties: - _atol (prop) - _components (prop) - _conform_to_aggregated_units (method) - _custom (prop) - _dask_meta (prop) - - """ - - def __init__(self, - filename, - address, - extent=None, - fragment_location=None, - dtype=None, - shape=None, - aggregated_units=None, - aggregated_calendar=None, - **kwargs): - - """ - Wrapper object for the 'array' section of a fragment. Contains some metadata to ensure the - correct fragment is selected, but generally just serves the fragment array to dask when required. - - Parameters: extent - in the form of a list/tuple of slices for this fragment. Different from the - 'location' parameter which states where the fragment fits into the total array. - """ - - self.__array_function__ = self.get_array - - self.filename = filename - self.address = address - self.extent = extent - self.dtype = dtype - self.shape = shape - self.size = product(shape) - self.ndim = len(shape) - - # Required by dask for thread-safety. - self._lock = SerializableLock() - - self.fragment_location = fragment_location - self.aggregated_units = aggregated_units # cfunits conform method. - self.aggregated_calendar = aggregated_calendar - - def __getitem__(self, selection): - ds = self.get_array(extent=tuple(selection)) - return ds - - def get_array(self, extent=None): - ds = self.open() - # Use extent to just select the section and variable I'd actually like to deal with here. - - if not extent: - extent = self.extent - if extent and self.extent: - raise NotImplementedError( - "Nested selections not supported. " - f"FragmentWrapper.get_array supplied '{extent}' " - f"and '{self.extent}' as selections." - ) - - if '/' in self.address: - # Assume we're dealing with groups but we just need the data for this variable. - - addr = self.address.split('/') - group = '/'.join(addr[1:-1]) - varname = addr[-1] - - ds = ds.groups[group] - - else: - varname = self.address - - try: - array = ds.variables[varname] - except KeyError: - raise ValueError( - f"CFA fragment '{self.fragment_location}' does not contain " - f"the variable '{varname}'." - ) - - if extent: - print(f'Post-processed Extent: {extent}') - try: - # This may not be loading the data in the most efficient way. - # Current: Slice the NetCDF-Dataset object then write to numpy. - # - This should hopefully take into account chunks and loading - # only the required data etc. - - var = np.array(array[tuple(extent)]) - except IndexError: - raise ValueError( - f"Unable to select required 'extent' of {self.extent} " - f"from fragment {self.fragment_location} with shape {array.shape}" - ) - else: - var = np.array(array) - - return var - - def open(self): - """Must be implemented by child class to properly open different file types.""" - raise NotImplementedError - -class NetCDFFragmentWrapper(FragmentWrapper): - def open(self): # get lock/release lock - return netCDF4.Dataset(self.filename, mode='r') diff --git a/CFAPyX/group.py b/CFAPyX/group.py index f893248..0d0c2a7 100644 --- a/CFAPyX/group.py +++ b/CFAPyX/group.py @@ -1,9 +1,13 @@ +__author__ = "Daniel Westwood" +__contact__ = "daniel.westwood@stfc.ac.uk" +__copyright__ = "Copyright 2023 United Kingdom Research and Innovation" + from contextlib import suppress -class PropertyWrapper: +class VariableWrapper: """ Wrapper object for the ``ds.variables`` and ``ds.attributes`` objects which can handle - either ``global`` or ``group`` based variables. + either ``global`` or ``group`` based variables . """ def __init__(self, prop_sets): @@ -51,19 +55,23 @@ def __getattr__(self, attr): f'No such attribute: "{attr}' ) -class GroupedDatasetWrapper: +class CFAGroupWrapper: """ Wrapper object for the CFADataStore ``ds`` parameter, required to bypass the issue with groups in Xarray, where all variables outside the group are ignored. """ def __init__(self, var_sets, ds_sets): - self.variables = PropertyWrapper( + self.variables = VariableWrapper( var_sets, ) self._ds_sets = ds_sets + self.Conventions = '' + if hasattr(ds_sets[0],'Conventions'): + self.Conventions = ds_sets[0].Conventions + @classmethod def open(cls, root, group, mode): diff --git a/CFAPyX/partition.py b/CFAPyX/partition.py new file mode 100644 index 0000000..a59c785 --- /dev/null +++ b/CFAPyX/partition.py @@ -0,0 +1,456 @@ +__author__ = "Daniel Westwood" +__contact__ = "daniel.westwood@stfc.ac.uk" +__copyright__ = "Copyright 2023 United Kingdom Research and Innovation" + +# Chunk wrapper is common to both CFAPyX and XarrayActive +VERSION = 1.1 + +import numpy as np +import netCDF4 + +from itertools import product +from dask.utils import SerializableLock + +try: + from XarrayActive import ActiveChunk +except: + class ActiveChunk: + pass + +class ArrayLike: + """ + Container class for Array-like behaviour + """ + description = 'Container class for Array-Like behaviour' + + def __init__(self, shape, units=None, dtype=None): + + # Standard parameters to store for array-like behaviour + self.shape = shape + self.units = units + self.dtype = dtype + + # Shape-based properties (Lazy loading means this may change in some cases) + @property + def size(self): + """ + Size property is derived from the current shape. In an ``ArrayLike`` + instance the shape is fixed, but other classes may alter the shape + at runtime. + """ + return product(self.shape) + + @property + def ndim(self): + """ + ndim property is derived from the current shape. In an ``ArrayLike`` + instance the shape is fixed, but other classes may alter the shape + at runtime. + """ + return len(self.shape) + + def copy(self, **kwargs): + """ + Return a new basic ArrayLike instance. Ignores provided kwargs + this class does not require, but other inheritors may.""" + return ArrayLike( + self.shape, + **self.get_kwargs() + ) + + def get_kwargs(self): + """ + Get the kwargs provided to this class initially - for creating a copy.""" + return { + 'units':self.units, + 'dtype':self.dtype + } + +class SuperLazyArrayLike(ArrayLike): + """ + Container class for SuperLazy Array-Like behaviour. ``SuperLazy`` behaviour is + defined as Lazy-Slicing behaviour for objects that are below the 'Dask Surface', + i.e for object that serve as Dask Chunks.""" + + description = "Container class for SuperLazy Array-Like behaviour" + + def __init__(self, shape, named_dims=None, **kwargs): + """ + Adds an ``extent`` variable derived from the initial shape, + this can be altered by performing slices, which are not applied + 'Super-Lazily' to the data. + """ + + self._extent = [ + slice(0, i) for i in shape + ] + + self.named_dims = named_dims + + super().__init__(shape, **kwargs) + + def __getitem__(self, selection): + """ + SuperLazy behaviour supported by saving index information to be applied when fetching the array. + This is considered ``SuperLazy`` because Dask already loads dask chunks lazily, but a further lazy + approach is required when applying Active methods. + """ + newextent = self._combine_slices(selection) + return self.copy(newextent) + + @property + def shape(self): + """ + Apply the current ``extent`` slices to determine the current array shape, + given all current slicing operations. This replaces shape as a simple + attribute in ``ArrayLike``, on instantiation the ``_shape`` private attribute + is defined, and subsequent attempts to retrieve the ``shape`` will depend on + the current ``extent``. + """ + current_shape = [] + if not self._extent: + return self._shape + for d, e in enumerate(self._extent): + start = e.start or 0 + stop = e.stop or self.shape[d] + step = e.step or 1 + current_shape.append(int((stop - start)/step)) + return tuple(current_shape) + + @shape.setter + def shape(self, value): + self._shape = value + + def _combine_slices(self, newslice): + """ + Combine existing ``extent`` attribute with a new set of slices. + + :param newslice: (tuple) A set of slices to apply to the data + 'Super-Lazily', i.e the slices will be combined with existing information + and applied later in the process. + + :returns: The combined set of slices. + """ + + if len(newslice) != len(self.shape): + if hasattr(self, 'active'): + # Mean has already been computed. Raise an error here + # since we should be down to dealing with numpy arrays now. + raise ValueError( + "Active chain broken - mean has already been accomplished." + ) + + else: + raise ValueError( + "Compute chain broken - dimensions have been reduced already." + ) + + def combine_sliced_dim(old, new, dim): + + ostart = old.start or 0 + ostop = old.stop or self._shape[dim] + ostep = old.step or 1 + + nstart = new.start or 0 + nstop = new.stop or self._shape[dim] + nstep = new.step or 1 + + start = ostart + ostep*nstart + step = ostep * nstep + stop = start + step * (nstop - nstart) + return slice(start, stop, step) + + + if not self._extent: + return newslice + else: + extent = self._extent + for dim in range(len(newslice)): + extent[dim] = combine_sliced_dim(extent[dim], newslice[dim], dim) + return extent + + def get_extent(self): + return self._extent + + def copy(self, newextent=None): + """ + Create a new instance of this class with all attributes of the current instance, but + with a new initial extent made by combining the current instance extent with the ``newextent``. + Each ArrayLike class must overwrite this class to get the best performance with multiple + slicing operations. + """ + kwargs = self.get_kwargs() + if newextent: + kwargs['extent'] = self._combine_slices(newextent) + + new_instance = SuperLazyArrayLike( + self.shape, + **kwargs + ) + return new_instance + + def get_kwargs(self): + return { + 'named_dims': self.named_dims + } | super().get_kwargs() + +class ArrayPartition(ActiveChunk, SuperLazyArrayLike): + """ + Complete Array-like object with all proper methods for data retrieval. + May include methods from ``XarrayActive.ActiveChunk`` if installed.""" + + description = "Complete Array-like object with all proper methods for data retrieval." + + def __init__(self, + filename, + address, + shape=None, + position=None, + extent=None, + format=None, + **kwargs + ): + + """ + Wrapper object for the 'array' section of a fragment or chunk. Contains some metadata to ensure the + correct fragment/chunk is selected, but generally just serves the array to dask when required. + + :param filename: (str) The path to the data file from which this fragment or chunk is + derived, if known. Not used in this class other than to support a ``.copy`` mechanism of + higher-level classes like ``CFAPartition``. + + + + :param address: (str) The variable name/address within the underlying data file which this class represents. + + :param dtype: (obj) The datatype of the values represented in this Array-like class. + + :param units: (obj) The units of the values represented in this Array-like class. + + :param shape: (tuple) The shape of the partition represented by this class. + + + + :param position: (tuple) The position in ``index space`` into which this chunk belongs, this could be + ``fragment space`` or ``chunk space`` if using Active chunks. + + :param extent: (tuple) Initial set of slices to apply to this chunk. Further slices may be applied which + are concatenated to the original slice defined here, if present. For fragments this will be + the extent of the whole array, but when using Active chunks the fragment copies may only + cover a partition of the fragment. + + :param format: (str) The format type of the underlying data file from which this fragment or chunk is + derived, if known. Not used in this class other than to support a ``.copy`` mechanism of + higher-level classes like ``CFAPartition``. + """ + + self.__array_function__ = self.__array__ + + self.filename = filename + self.address = address + + self.format = format + self.position = position + + self._extent = extent + self._lock = SerializableLock() + + super().__init__(shape, **kwargs) + + def __array__(self, *args, **kwargs): + """ + Retrieves the array of data for this variable chunk, casted into a Numpy array. Use of this method + breaks the ``Active chain`` by retrieving all the data before any methods can be applied. + + :returns: A numpy array of the data for the correct variable with correctly applied selections + defined by the ``extent`` parameter. + """ + + # Unexplained xarray behaviour: + # If using xarray indexing, __array__ should not have a positional 'dtype' option. + # If casting DataArray to numpy, __array__ requires a positional 'dtype' option. + dtype = None + if args: + dtype = args[0] + + if dtype != self.dtype: + raise ValueError( + 'Requested datatype does not match this chunk' + ) + + ds = self.open() + + if '/' in self.address: + # Assume we're dealing with groups but we just need the data for this variable. + + addr = self.address.split('/') + group = '/'.join(addr[1:-1]) + varname = addr[-1] + + ds = ds.groups[group] + + else: + varname = self.address + + try: + array = ds.variables[varname] + except KeyError: + raise ValueError( + f"Dask Chunk at '{self.position}' does not contain " + f"the variable '{varname}'." + ) + + if hasattr(array, 'units'): + self.units = array.units + + if len(array.shape) != len(self._extent): + self._correct_slice(array.dimensions) + + try: + var = np.array(array[tuple(self._extent)]) + except IndexError: + raise ValueError( + f"Unable to select required 'extent' of {self.extent} " + f"from fragment {self.position} with shape {array.shape}" + ) + + return self._post_process_data(var) + + def _correct_slice(self, array_dims): + """ + Drop size-1 dimensions from the set of slices if there is an issue. + + :param array_dims: (tuple) The set of named dimensions present in + the source file. If there are fewer array_dims than the expected + set in ``named_dims`` then this function is used to remove extra + dimensions from the ``extent`` if possible. + """ + extent = [] + for dim in range(len(self.named_dims)): + named_dim = self.named_dims[dim] + if named_dim in array_dims: + extent.append(self._extent[dim]) + + # named dim not present + ext = self._extent[dim] + + start = ext.start or 0 + stop = ext.stop or self.shape[dim] + step = ext.step or 1 + + if int(stop - start)/step > 1: + raise ValueError( + f'Attempted to slice dimension "{named_dim}" using slice "{ext}" ' + 'but the requested dimension is not present' + ) + self._extent = extent + + def _post_process_data(self, data): + """ + Perform any post-processing steps on the data here. + - unit correction + - calendar correction + """ + return data + + def _try_openers(self, filename): + """ + Attempt to open the dataset using all possible methods. Currently only NetCDF is supported. + """ + for open in [ + self._open_netcdf, + self._open_pp, + self._open_um + ]: + try: + ds = open(filename) + except: + pass + if not ds: + raise FileNotFoundError( + 'No file type provided and opening failed with all known types.' + ) + return ds + + def _open_pp(self, filename): + raise NotImplementedError + + def _open_um(self, filename): + raise NotImplementedError + + def _open_netcdf(self, filename): + """ + Open a NetCDF file using the netCDF4 python package.""" + return netCDF4.Dataset(filename, mode='r') + + def get_kwargs(self): + """ + Return all the initial kwargs from instantiation, to support ``.copy()`` mechanisms by higher classes. + """ + return { + 'shape': self.shape, + 'position': self.position, + 'extent': self._extent, + 'format': self.format + } | super().get_kwargs() + + def copy(self, newextent=None): + """ + Create a new instance of this class with all attributes of the current instance, but + with a new initial extent made by combining the current instance extent with the ``newextent``. + Each ArrayLike class must overwrite this class to get the best performance with multiple + slicing operations. + """ + kwargs = self.get_kwargs() + if newextent: + kwargs['extent'] = self._combine_slices(newextent) + + new_instance = ArrayPartition( + self.filename, + self.address, + **kwargs, + ) + return new_instance + + def open(self): + """ + Open the source file for this chunk to extract data. Multiple file locations may be provided + for this object, in which case there is a priority for 'remote' sources first, followed by + 'local' sources - otherwise the order is as given in the fragment array variable ``location``. + """ + + filenames = self.filename + + if type(filenames) == str: + filenames = [filenames] + + # Tidy code - never going to be many filenames + local = [l for l in filenames if '://' not in l] + remote = [r for r in filenames if '://' in r] + relative = [d for d in filenames if d[:5] not in ('https','s3://','file:')] + + # Prioritise relative then remote options first if any are present. + filenames = relative + remote + local + + for filename in filenames: + try: + if not self.format: + # guess opening format. + return self._try_openers(filename) + + if self.format == 'nc': + return self._open_netcdf(filename) + else: + raise ValueError( + f"Unrecognised format '{self.format}'" + ) + except ValueError as err: + raise err + except: + pass + + raise FileNotFoundError( + f'None of the location options for chunk "{self.position}" could be accessed.' + f'Locations tried: {filenames}.' + ) + diff --git a/CFAPyX/utils.py b/CFAPyX/utils.py deleted file mode 100644 index 0ce89ce..0000000 --- a/CFAPyX/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -import numpy as np - -class OneOrMoreList: - """ - Simple list wrapper object such that a list of length 1 provides its - single item when indexed at any ordinal position. - """ - - def __init__(self, array): - self.array = array - self.is_one = len(array) == 1 - - def __getitem__(self, index): - if self.is_one: - return self.array[0] - else: - return self.array[index] - -def _ensure_fill_value_valid(data, attributes): - """ - Private Xarray function required in CFAPyX.datastore.CFADataStore, hence a copy is placed here. - """ - # work around for netCDF4/scipy issue where _FillValue has the wrong type: - # https://github.com/Unidata/netcdf4-python/issues/271 - if data.dtype.kind == "S" and "_FillValue" in attributes: - attributes["_FillValue"] = np.bytes_(attributes["_FillValue"]) \ No newline at end of file diff --git a/CFAPyX/wrappers.py b/CFAPyX/wrappers.py new file mode 100644 index 0000000..15ac658 --- /dev/null +++ b/CFAPyX/wrappers.py @@ -0,0 +1,590 @@ +__author__ = "Daniel Westwood" +__contact__ = "daniel.westwood@stfc.ac.uk" +__copyright__ = "Copyright 2023 United Kingdom Research and Innovation" + +from CFAPyX.decoder import get_dask_chunks +from CFAPyX.partition import ArrayPartition, ArrayLike + +import dask.array as da +from dask.array.core import getter +from dask.base import tokenize +from dask.utils import SerializableLock, is_arraylike +from dask.array.reductions import numel + +from itertools import product +import netCDF4 +import numpy as np + +class FragmentArrayWrapper(ArrayLike): + """ + FragmentArrayWrapper behaves like an Array that can be indexed or referenced to + return a Dask-like array object. This class is essentially a constructor for the + partitions that feed into the returned Dask-like array into Xarray. + """ + + description = 'Wrapper-class for the array of fragment objects' + + def __init__( + self, + fragment_info, + fragment_space, + shape, + units, + dtype, + cfa_options={}, + active_options={}, + named_dims=None + ): + """ + Initialisation method for the FragmentArrayWrapper class + + :param fragment_info: (dict) The information relating to each fragment with the + fragment coordinates in ``fragment space`` as the key. Each + fragment is described by the following: + - ``shape`` - The shape of the fragment in ``array space``. + - ``location`` - The file from which this fragment originates. + - ``address`` - The variable and group name relating to this variable. + - ``extent`` - The slice object to apply to the fragment on retrieval (usually get + the whole array) + - ``global_extent`` - The slice object that equates to a particular fragment out + of the whole array (in ``array space``). + + :param fragment_space: (tuple) The coordinate system that refers to individual + fragments. Each coordinate eg. i, j, k refers to the + number of fragments in each of the associated dimensions. + + :param shape: (tuple) The total shape of the array in ``array space`` + + :param units: (obj) The units of the values represented in this Array-like class. + + :param dtype: (obj) The datatype of the values represented in this Array-like class. + + :param cfa_options: (dict) The set of options defining some specific decoding behaviour. + + :param active_options: (dict) The set of options defining Active behaviour. + + :param named_dims: (list) The set of dimension names that apply to this Array object. + + :returns: None + """ + + self.fragment_info = fragment_info + self.fragment_space = fragment_space + self.named_dims = named_dims + + # Set internal private variables + self.cfa_options = cfa_options + self.active_options = active_options + + self.__array_function__ = self.__array__ + + super().__init__(shape, dtype=dtype, units=units) + + def __getitem__(self, selection): + """ + Non-lazy retrieval of the dask array when this object is indexed. + """ + arr = self.__array__() + return arr[selection] + + def __array__(self): + """ + Non-lazy array construction, this will occur as soon as the instance is ``indexed`` + or any other ``array`` behaviour is attempted. Construction of a Dask-like array + occurs here based on the decoded fragment info and any other specified settings. + """ + + array_name = (f"{self.__class__.__name__}-{tokenize(self)}",) + + dtype = self.dtype + units = self.units + + calendar = None # Fix later + + # Fragment info dict at this point + fragment_info = self.fragment_info + + # For now expect to deal only with NetCDF Files + + # dict of array-like objects to pass to the dask Array constructor. + fragments = {} + + for pos in self.fragment_info.keys(): + + fragment_shape = self.fragment_info[pos]['shape'] + fragment_position = pos + global_extent = self.fragment_info[pos]['global_extent'] + extent = self.fragment_info[pos]['extent'] + + fragment_format = 'nc' + + if 'fill_value' in self.fragment_info[pos]: + filename = None + address = None + # Extra handling required for this condition. + else: + filename = self.fragment_info[pos]['location'] + address = self.fragment_info[pos]['address'] + + # Wrong extent type for both scenarios but keep as a different label for + # dask chunking. + + fragment = CFAPartition( + filename, + address, + dtype=dtype, + extent=extent, + shape=fragment_shape, + position=fragment_position, + aggregated_units=units, + aggregated_calendar=calendar, + format=fragment_format, + named_dims=self.named_dims + ) + + fragments[pos] = fragment + + if not self._active_chunks: + dsk = self._chunk_by_fragment(fragments) + + global_extent = { + k: fragment_info[k]["global_extent"] for k in fragment_info.keys() + } + + dask_chunks = get_dask_chunks( + self.shape, + self.fragment_space, + extent=global_extent, + dtype=self.dtype, + explicit_shapes=None + ) + + else: + dsk = self._chunk_oversample(fragments) + dask_chunks = None # Assemble in the same format as the CFA ones. + raise NotImplementedError + + if self._use_active: + try: + from XarrayActive import DaskActiveArray + + darr = DaskActiveArray(dsk, array_name[0], chunks=dask_chunks, dtype=dtype) + except ImportError: + raise ImportError( + '"DaskActiveArray" from XarrayActive failed to import - please ensure ' + 'you have the XarrayActive package installed.' + ) + else: + darr = da.Array(dsk, array_name[0], chunks=dask_chunks, dtype=dtype) + return darr + + @property + def active_options(self): + """ + Relates private option variables to the ``active_options`` parameter of the + backend. + """ + + return { + 'use_active': self._use_active, + 'active_chunks': self._active_chunks + } + + @active_options.setter + def active_options(self, value): + self._set_active_options(**value) + + def _set_active_options( + self, + use_active=False, + active_chunks=None, + **kwargs): + """ + Sets the private variables referred by the ``active_options`` parameter to the backend. + Ignores additional kwargs. + """ + + self._use_active = use_active + self._active_chunks = active_chunks + + @property + def cfa_options(self): + """ + Relates private option variables to the ``cfa_options`` parameter of the backend. + """ + + return { + 'substitutions': self._substitutions, + 'decode_cfa': self._decode_cfa + } + + @cfa_options.setter + def cfa_options(self, value): + self._set_cfa_options(**value) + + def _set_cfa_options( + self, + substitutions=None, + decode_cfa=None, + **kwargs): + """ + Sets the private variables referred by the ``cfa_options`` parameter to the backend. + Ignores additional kwargs. + """ + + # Don't need this here + if substitutions: + + if type(substitutions) != list: + substitutions = [substitutions] + + for s in substitutions: + base, substitution = s.split(':') + for f in self.fragment_info.keys(): + self.fragment_info[f]['location'] = self.fragment_info[f]['location'].replace(base, substitution) + + self._substitutions = substitutions + self._decode_cfa = decode_cfa + + def _chunk_by_fragment(self, fragments): + """ + Assemble the base ``dsk`` task dependency graph which includes the fragment objects + plus the method to index each object (with locking). + + :param fragments: (dict) The set of Fragment objects (CFAPartition) with + their positions in ``fragment space``. + + :returns: A task dependency graph with all the fragments included to use + when constructing the dask array. + """ + array_name = (f"{self.__class__.__name__}-{tokenize(self)}",) + + dsk = {} + for fragment_position in fragments.keys(): + fragment = fragments[fragment_position] + + f_identifier = f"{fragment.__class__.__name__}-{tokenize(fragment)}" + dsk[f_identifier] = fragment + dsk[array_name + fragment_position] = ( + getter, + # Method of retrieving the 'data' from each fragment - but each fragment is Array-like. + f_identifier, + fragment.get_extent(), + False, + getattr(fragment, "_lock", False) # Check version cf-python + ) + return dsk + + def _derive_chunk_space(self): + """ + Derive the chunk space and shape given the user-provided ``active_chunks`` option. + Chunk space is the number of chunks in each dimension which presents like an array + shape, but is referred to as a ``space`` because it has a novel coordinate system. + Chunk shape is the shape of each chunk in ``array space``, which must be regular + even if lower-level objects used to define the chunk are not. + + Example: + 50 chunks across the time dimension of 1000 values which is represented by 8 + fragments. Chunk space representation is (50,) and the chunk shape is (20,). + + Each chunk is served by at most 2 fragments, where each chunk is described using a + MultiFragmentWrapper object which appropriately sets the extents of each Fragment + object. The Fragments cover 125 values each: + + Chunk 0 served by Fragment 0 slice(0,20) + Chunk 1 served by Fragment 0 slice(20,40) + ... + Chunk 6 served by Fragment 0 slice(120,None) and Fragment 1 slice(0,15) + ... + and so on. + + """ + chunk_space = [1 for i in self.shape] + chunk_shape = [i for i in self.shape] + + for dim in self.active_chunks.keys(): + chunks = self.active_chunks[dim] + + idim = None + for x, d in enumerate(self.named_dims): + if d == dim: + idim = x + + if not idim: + raise ValueError( + f"Requested chunking across dimension '{dim}'" + f"but only '{self.named_dims}' present in the dataset" + ) + + length = self.shape[idim] + chunk_space[idim] = chunks + chunk_shape[idim] = int(length/chunks) + + return chunk_space, chunk_shape + + def _chunk_oversample(self, fragments): + """ + Assemble the base ``dsk`` task dependency graph which includes the chunk + objects plus the method to index each chunk object (with locking). In this + case, each chunk object is a MultiFragmentWrapper which serves another dask array + used to combine the individual fragment arrays contributing to each chunk. + + :param fragments: (dict) The set of Fragment objects (ChunkWrapper/CFAChunk) with + their positions in ``fragment space``. These are copied into MultiFragmentWrappers + with the correctly applied extents such that all the chunks define the scope of the + total array. + + Terminology Notes: + + ``cs`` and ``fs`` represent the chunk_shape and fragment_shape respectively, + with short names to make the code simpler to read. + + :returns: A task dependency graph with all the chunks included to use when + constructing the dask array. + """ + + chunk_space, cs = self._derive_chunk_space() + + mfwrapper = {} + + for fragment_coord in fragments.keys(): + + fragment = fragments[fragment_coord] + + # Shape of each fragment may vary + fs = fragment.shape + + # Calculate chunk coverage for this fragment + initial, final = [],[] + for dim in range(len(fragment_coord)): + + initial.append( + int(fragment_coord[dim] * fs[dim]/cs[dim]) + ) + fn = int((fragment_coord[dim]+1) * fs[dim]/cs[dim]) + + final.append( + min(fn, chunk_space[dim]) + ) + + # Chunk coverage extent + # - Two chunk-space coordinates defining the chunks that are covered by this + # fragment. + cce = [tuple(initial), tuple(final)] + + # Generate the list of chunks covered by this fragment. + chunk_list = [ + coord for coord in product( + *[range(r[0], r[1]) for r in zip(cce[0], cce[1])] + ) + ] + + # Generate the 'extent of this fragment' in ``fragment_space`` + # i.e Fragment (0,0) has extent (0,0) to (1,1) + fragment_extent = [ + tuple(fragment_coord), + (i +1 for i in fragment_coord) + ] + + # Copy the fragment with the correct extent for each chunk covered. + for c in chunk_list: + # For each fragment, the subdivisions caused by chunking create an irregular + # array of sliced fragments which comprises the whole chunk. Each of these + # sliced fragments needs a coordinate relative to the chunk it is being + # assigned to. + relative_fragment = tuple([c[i] - chunk_list[0][i] for i in range(len(c))]) + + chunk = [ + tuple(c), + (i+1 for i in c) + ] + + hyperslab = _overlap(chunk, cs, fragment_extent, fs) + + newfragment = fragment.copy(extent=hyperslab) + + if c in mfwrapper: + mfwrapper[c][relative_fragment] = newfragment + else: + mfwrapper[c] = {relative_fragment: newfragment} + + array_name = (f"{self.__class__.__name__}-{tokenize(self)}",) + dsk = {} + + for chunk in mfwrapper.keys(): + fragments = mfwrapper[chunk] + + mfwrap = CFAChunkWrapper(fragments) + + # f_indices is the initial_extent for the ArrayPartition + + mf_identifier = f"{mfwrap.__class__.__name__}-{tokenize(mfwrap)}" + dsk[mf_identifier] = mfwrap + dsk[array_name + chunk] = ( + getter, # From dask docs - replaces fragment_getter + mf_identifier, + fragment.get_extent(), # Needs a think on how to get this out. + False, + getattr(fragment, "_lock", False) # Check version cf-python + ) + return dsk + +class CFAChunkWrapper(ArrayLike): + description = 'Brand new array class for handling any-size dask chunks.' + + """ + Requirements: + - Fragments are initialised with a position in index space. (Fragment Space) + - Chunk position array initialised with a different index space. (Compute Space) + - For each fragment, identify which chunk positions it falls into and add that + `CFAPartition` to a dict. + - The dict contains Chunk coordinates (compute space) as keys, with the values being + a list of pairs of CFAPartition objects that are already sliced and the array shapes + those sliced segments fit into. + """ + + def __init__(self, fragments): + self.fragments = fragments + + raise NotImplementedError + + def __array__(self): + array_name = (f"{self.__class__.__name__}-{tokenize(self)}",) + + dsk = {} + for fragment_position in self.fragments.keys(): + fragment = self.fragments[fragment_position] + # f_indices is the initial_extent for the ArrayPartition + + f_identifier = f"{fragment.__class__.__name__}-{tokenize(fragment)}" + dsk[f_identifier] = fragment + dsk[array_name + fragment_position] = ( + getter, # From dask docs - replaces fragment_getter + f_identifier, + fragment.get_extent(), + False, + getattr(fragment, "_lock", False) # Check version cf-python + ) + + # Should return a dask array. + return dsk + +class CFAPartition(ArrayPartition): + """ + Wrapper object for a CFA Partition, extends the basic ArrayPartition with CFA-specific + methods. + """ + + description = 'Wrapper object for a CFA Partition (Fragment or Chunk)' + + + def __init__(self, + filename, + address, + aggregated_units=None, + aggregated_calendar=None, + **kwargs + ): + + """ + Wrapper object for the 'array' section of a fragment. Contains some metadata + to ensure the correct fragment is selected, but generally just serves the + fragment array to dask when required. + + :param filename: (str) The path to a Fragment file from which this + partition object will access data from. The partition may represent + all or a subset of the data from the Fragment file. + + :param address: (str) The address of the data variable within the + Fragment file, may include a group hierarchy structure. + + :param aggregated_units: (obj) The expected units for the received data. + If the units of the received data are not equal to the ``aggregated_units`` + then the data is 'post-processed' using the cfunits ``conform`` function. + + :param aggregated_calendar: None + """ + + super().__init__(filename, address, units=aggregated_units, **kwargs) + self.aggregated_units = aggregated_units + self.aggregated_calendar = aggregated_calendar + + def copy(self, extent=None): + """ + Create a new instance of this class from its own methods and attributes, and + apply a new extent to the copy if required. + """ + + kwargs = self.get_kwargs() + + if 'units' in kwargs: + if not kwargs['aggregated_units']: + kwargs['aggregated_units'] = kwargs['units'] + kwargs.pop('units') + + if extent: + kwargs['extent'] = self._combine_slices(extent) + + new = CFAPartition( + self.filename, + self.address, + **kwargs + ) + return new + + def _post_process_data(self, data): + """Correct units/data conversions - if necessary at this stage""" + + if self.units != self.aggregated_units: + try: + from cfunits import Units + except FileNotFoundError: + raise ValueError( + 'Encountered issue when trying to import the "cfunits" library:' + "cfunits requires UNIDATA UDUNITS-2. Can't find the 'udunits2' library." + ' - Consider setting up a conda environment, and installing ' + '`conda install -c conda-forge udunits2`' + ) + + data = Units.conform(data, self.units, self.aggregated_units) + return data + + def get_kwargs(self): + return { + 'aggregated_units': self.aggregated_units, + 'aggregated_calendar': self.aggregated_calendar + } | super().get_kwargs() + +def _overlap(chunk, chunk_size, fragment, fragment_size): + """ + Determine the overlap between a chunk and fragment. Not yet properly implemented. + + :param chunk: None + + :param chunk_size: None + + :param fragment: None + + :param fragment_size: None + + Chunk and Fragment need to have structure (2,N) where 2 signifies the start and end + of each dimension and N is the number of dimensions. + """ + + extent = [] + for dim in range(len(chunk[0])): + dimslice = _overlap_in_1d( + (chunk[0][dim], chunk[1][dim]), + chunk_size, + (fragment[0][dim], fragment[1][dim]), + fragment_size + ) + extent.append(dimslice) + return extent # Total slice-based overlap of chunk and fragment + +def _overlap_in_1d(chunk, chunk_size, fragment, fragment_size): + + start = max(chunk[0]*chunk_size, fragment[0]*fragment_size) + end = min(chunk[1]*chunk_size, fragment[1]*chunk_size) + + return slice(start, end) # And possibly more \ No newline at end of file diff --git a/README.md b/README.md index f0b63b5..86718e9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ -# CFAPyX +![CFAPyX long logo: Blue, Green and White squares arranged in Diamond formation](https://github.com/cedadev/CFAPyX/blob/CF1.12/docs/source/_images/CFAPyX_long.jpg) + CFA python Xarray module for using CFA files with xarray. See the [Documentation](https://cedadev.github.io/CFAPyX/) for more details. For use with the Xarray module as an additional backend. -## Installation +# Installation ``` pip install xarray==2024.6.0 pip install -e . ``` -## Usage +# Usage ``` import xarray as xr diff --git a/docs/source/_images/CFAPyX.ico b/docs/source/_images/CFAPyX.ico new file mode 100644 index 0000000..96d5fcd Binary files /dev/null and b/docs/source/_images/CFAPyX.ico differ diff --git a/docs/source/_images/CFAPyX_logo.png b/docs/source/_images/CFAPyX_logo.png new file mode 100644 index 0000000..59a8904 Binary files /dev/null and b/docs/source/_images/CFAPyX_logo.png differ diff --git a/docs/source/_images/CFAPyX_logo.pptx b/docs/source/_images/CFAPyX_logo.pptx new file mode 100644 index 0000000..8df9c7c Binary files /dev/null and b/docs/source/_images/CFAPyX_logo.pptx differ diff --git a/docs/source/_images/CFAPyX_long.jpg b/docs/source/_images/CFAPyX_long.jpg new file mode 100644 index 0000000..5373488 Binary files /dev/null and b/docs/source/_images/CFAPyX_long.jpg differ diff --git a/docs/source/_images/CFAPyX_long.png b/docs/source/_images/CFAPyX_long.png new file mode 100644 index 0000000..debaf0a Binary files /dev/null and b/docs/source/_images/CFAPyX_long.png differ diff --git a/docs/source/_images/CFAPyX_nowords.png b/docs/source/_images/CFAPyX_nowords.png new file mode 100644 index 0000000..1052297 Binary files /dev/null and b/docs/source/_images/CFAPyX_nowords.png differ diff --git a/docs/source/_images/CFAPyX_structure.png b/docs/source/_images/CFAPyX_structure.png new file mode 100644 index 0000000..2e505fe Binary files /dev/null and b/docs/source/_images/CFAPyX_structure.png differ diff --git a/docs/source/backendentrypoint.rst b/docs/source/backend.rst similarity index 75% rename from docs/source/backendentrypoint.rst rename to docs/source/backend.rst index ad6e7f3..3d74544 100644 --- a/docs/source/backendentrypoint.rst +++ b/docs/source/backend.rst @@ -2,5 +2,5 @@ Xarray Backend Entrypoint for CFAPyX ==================================== -.. automodule:: CFAPyX.backendentrypoint +.. automodule:: CFAPyX.backend :members: \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index 0a78115..35a7f10 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,13 +10,13 @@ import sys sys.path.insert(0, os.path.abspath('../../')) -project = 'CFAPyX' +project = 'CFAPyX Package' copyright = ('2022-2024, Centre of Environmental Data Analysis Developers,' 'Scientific and Technical Facilities Council (STFC),' 'UK Research and Innovation (UKRI). ' - 'BSD 2-Clause License. All rights reserved.') + 'BSD 2-Clause License. All rights reserved') author = 'Daniel Westwood' -release = 'v1.1' +release = '2024.8.9' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -24,12 +24,15 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', - 'sphinx.ext.autosectionlabel' + 'sphinx.ext.autosectionlabel', + 'sphinx.ext.viewcode' ] templates_path = ['_templates'] exclude_patterns = [] +autodoc_member_order = 'bysource' + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output @@ -37,7 +40,8 @@ html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] -#html_logo = '_images/ceda.png' -html_favicon = '_images/favicon.ico' +html_logo = '_images/CFAPyX_long.png' +html_favicon = '_images/CFAPyX.ico' html_a1 = '_images/ceda.png' html_a2 = '_images/ncas.png' +html_a3 = '_images/CFAPyX_nobg.png' diff --git a/docs/source/fragmentarray.rst b/docs/source/fragmentarray.rst deleted file mode 100644 index 787efc2..0000000 --- a/docs/source/fragmentarray.rst +++ /dev/null @@ -1,6 +0,0 @@ -================================ -CFA Fragment Arrays and Wrappers -================================ - -.. automodule:: CFAPyX.fragmentarray - :members: \ No newline at end of file diff --git a/docs/source/fragments.rst b/docs/source/fragments.rst new file mode 100644 index 0000000..e1ca8d6 --- /dev/null +++ b/docs/source/fragments.rst @@ -0,0 +1,5 @@ +================ +Fragments in CFA +================ + +Coming Soon! \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 058c434..0c6280a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,15 +18,23 @@ Current support is limited to local netCDF4 formatted files, but future addition .. toctree:: :maxdepth: 1 - :caption: Contents: + :caption: Details: Inspiration for CFA Xarray Engine Overview - CFA Backend Entrypoint + Fragments, Partitions and Chunks + CFAPyX Usage and Options + + +.. toctree:: + :maxdepth: 1 + :caption: API Reference + + CFA Backend Entrypoint CFA DataStore - CFA Fragment Array - NetCDF group handling - Utilities + CFA Wrappers + Partitions + CFA Groups Indices and Tables ================== diff --git a/docs/source/inspiration.rst b/docs/source/inspiration.rst index 01cbacc..bbe0d49 100644 --- a/docs/source/inspiration.rst +++ b/docs/source/inspiration.rst @@ -2,3 +2,4 @@ CFA and Distributed Data ======================== +Coming Soon! \ No newline at end of file diff --git a/docs/source/options.rst b/docs/source/options.rst new file mode 100644 index 0000000..820a229 --- /dev/null +++ b/docs/source/options.rst @@ -0,0 +1,5 @@ +======================== +CFAPyX Usage and Options +======================== + +Coming Soon! \ No newline at end of file diff --git a/docs/source/partition.rst b/docs/source/partition.rst new file mode 100644 index 0000000..7bcddcc --- /dev/null +++ b/docs/source/partition.rst @@ -0,0 +1,13 @@ +============== +CFA Partitions +============== + +.. Note:: + The ``partition.py`` script is shared across CFAPyX and XarrayActive. It has its own version number (1.0) and is + currently manually updated across both versions. Future developments may include a way of keeping track of this + in a more sophisticated manner. + + +.. automodule:: CFAPyX.partition + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/utils.rst b/docs/source/utils.rst deleted file mode 100644 index a1cbc53..0000000 --- a/docs/source/utils.rst +++ /dev/null @@ -1,8 +0,0 @@ -================ -Shared Utilities -================ - -The utility module is a store for miscellaneous useful functions that do not belong to one of the specific areas of the engine. - -.. automodule:: CFAPyX.utils - :members: \ No newline at end of file diff --git a/docs/source/wrapper.rst b/docs/source/wrapper.rst new file mode 100644 index 0000000..ea0c837 --- /dev/null +++ b/docs/source/wrapper.rst @@ -0,0 +1,8 @@ +========================= +CFA Array Wrapper Classes +========================= + +.. automodule:: CFAPyX.wrappers + :members: + :special-members: + :show-inheritance: \ No newline at end of file diff --git a/fetch_func/fetch_functions.py b/fetch_func/fetch_functions.py deleted file mode 100644 index d443cf9..0000000 --- a/fetch_func/fetch_functions.py +++ /dev/null @@ -1,53 +0,0 @@ -# Give this the path to a module. -import os -import re -# Highlight specific functions - -# Could take all global variables and track back to origin as well - could be a useful feature. - -function_list = [ - 'fsspec.mapping.url_to_fs' -] - -def extract_function(lines, func): - line_count = 0 - started = False - finished = False - func_content = '' - while not finished and line_count < len(lines): - line = lines[line_count] - - if started and re.search(f'def ', lines[line_count+1]): - finished = True - - if re.search(f'def {func}', line): - started = True - - if started: - func_content += line - - line_count += 1 - - return func_content - -def search_function(base, fpath, fname): - script_path = os.path.join(base, os.path.join(*fpath)) + '.py' - - with open(script_path) as f: - contents = f.readlines() - - func = extract_function(contents, fname) - return func - -base = '/home/users/dwest77/Documents/cfa_python_dw/cf_dw/xarray' - -collection = '## NOTE: Not a script which is meant to be run\n\n\n' - -for f in function_list: - fpath = f.split('.') - fname = fpath[-1] - fpath.pop() - collection += search_function(base, fpath, fname) - -with open('usefuls.py','w') as f: - f.write(collection) diff --git a/fetch_func/usefuls.py b/fetch_func/usefuls.py deleted file mode 100644 index 63303e3..0000000 --- a/fetch_func/usefuls.py +++ /dev/null @@ -1,17 +0,0 @@ -## NOTE: Not a script which is meant to be run - - -def ensure_not_multiindex(var: Variable, name: T_Name = None) -> None: - # only the pandas multi-index dimension coordinate cannot be serialized (tuple values) - if isinstance(var._data, indexing.PandasMultiIndexingAdapter): - if name is None and isinstance(var, IndexVariable): - name = var.name - if var.dims == (name,): - raise NotImplementedError( - f"variable {name!r} is a MultiIndex, which cannot yet be " - "serialized. Instead, either use reset_index() " - "to convert MultiIndex levels into coordinate variables instead " - "or use https://cf-xarray.readthedocs.io/en/latest/coding.html." - ) - - diff --git a/requirements.txt b/requirements.txt index 4855377..3ec3a50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ netCDF4==1.6.5 h5py==3.11.0 dask==2024.7.0 cftime==1.6.4 -cfunits==3.3.7 \ No newline at end of file +cfunits==3.3.7 +pytest==7.2.0 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index 228a691..91b11ee 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,4 +3,5 @@ netCDF4==1.6.5 h5py==3.11.0 dask==2024.7.0 cftime==1.6.4 -cfunits==3.3.7 \ No newline at end of file +cfunits==3.3.7 +pytest==7.2.0 \ No newline at end of file diff --git a/testing/CMIPCFA.ipynb b/testing/CMIPCFA.ipynb deleted file mode 100644 index 1f22d42..0000000 --- a/testing/CMIPCFA.ipynb +++ /dev/null @@ -1,625 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "2533f639-1d37-451d-b40b-e4df1238a0cc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 1.32 s, sys: 1.12 s, total: 2.44 s\n", - "Wall time: 1.57 s\n" - ] - } - ], - "source": [ - "%%time\n", - "import xarray as xr\n", - "cfafile = '../testfiles/ScenarioMIP.nca'\n", - "ds = xr.open_dataset(cfafile,engine='CFA')" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "20bdae04-a448-4eaa-84e1-b420ddcb687f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 6.85 ms, sys: 0 ns, total: 6.85 ms\n", - "Wall time: 6.79 ms\n" - ] - } - ], - "source": [ - "%%time\n", - "h1 = ds['huss'].sel(lat=slice(-60,0), lon=slice(80,180)).isel(time=slice(10000,12000)).mean(dim='time')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "7810becc-1292-4637-b142-4858f14ca881", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Post-processed Extent: (slice(10000, 12000, None), slice(21, 64, None), slice(57, 129, None))\n", - "CPU times: user 1.31 s, sys: 92.4 ms, total: 1.4 s\n", - "Wall time: 1.43 s\n" - ] - } - ], - "source": [ - "%%time\n", - "h2 = h1.compute()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "9922b1c5-ceee-466d-af5f-67e9ca149d2a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 377 ms, sys: 35.8 ms, total: 413 ms\n", - "Wall time: 477 ms\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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/vrXv2LWrFk4cOAAioqK0KVLF0RHR7ujfx6jL7MfSbovO8kF715RBooL+qstk8n553OU7Lm0JFRralj0XBrSvrXkuNyMFEFukSzrSUt+kyj3qjxQ1gv1TjaUOf6GkL1GOkn6urANgyBjS9KuKFNJltnjtgwzZ783nHwuGZd8d4nSviWp+4rgt6C0C9e/nrJsLLrlVDuUslmzZmjWrJkr+0JEREQiDKX0KIcGS8nJyQ43uHjx4mp3hoiIiNR4uRPPcmiwlJWVZffznj17UFFRgTZt2gAADh8+DIPBgK5du7q+h0REREQe5NBgadu2bbb/L168GHXr1sWqVatQv359AMD58+cxfPhw3HPPPe7pJRERUW3Gw3AepXnp6aJFizBv3jzbQAkA6tevj1deeQWLFi1yaeeIiIiI0QGepnmwVFhYiDNnzqjKz5w5g4sXL7qkU0RERETeQvNg6fHHH8fw4cOxfv16/P777/j999/x8ccfY8SIEejfv787+khERFS7MWfJozRHB6SmpuKFF17AU089hfLyK8EgPj4+GDFiBBYuXOjyDhIREdV6XLPkUZoHSwEBAfjLX/6ChQsX4ujRowCAVq1aITBQmhZ3U/IpUWCwXn1nueRYr6gNWW6em97U4oA7x5+sJsMntXI2k1JLA04HYEIcuuj8RlTxfO5qW0uYp6gPkn6J6sq2ocJffYfVV9awur/C1wLiUEmrr6QPfuo2RIGHwJXQW3VlcV0R6UspCpoUBDRq5mSApfS95+x7UtIHnxJ1mbHI8R1RVkd80KXC/7qnlwSJuoMOzu8uN3693PKqHUoZGBiITp06ubIvRERERF5H82Dp/vvvh05weYNKW7dudapDREREdB0ehvMozYOlmJgYu5/Ly8uxd+9eHDhwAElJSa7qFxEREf0PE7w9S/NgacmSJcLyOXPmoKioyOkOEREREXkTl10P/emnn8a7777rquaIiIioEqMDPKraC7yvl5GRAT8/P1c1R0RERNfiYMdjNA+Wrg+eVBQFp06dwu7duzFr1iyXdYyIiIjIG2geLAUFBdmdDafX69GmTRu89NJL6N27t0s7R0RERFzg7WmaB0vvv/++G7rhuNLSUsTGxmLfvn3IysqyOztv//79GDNmDH788UeEhoZi7NixmDJlSrWex1CiwMdSvXeWawIsb/539a38wZSH7AmCCaV1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794dJpMJjRs3xrRp01BRUWG7/7777sO4ceMwZcoUNGjQAOHh4ZgzZ45dG4cOHULPnj3h5+eH9u3b46uvvrI7NHbtYbjjx4/j/vvvBwDUr18fOp0Ow4YNAwA0b95cNasQExNj93xHjhzBvffea3uu9PR01Tbl5ubiySefRL169dCgQQP069cPx48f17xviouLMXToUNSpUweNGzfGokWLVHVKS0vxwgsvoEmTJggMDERsbCy+/vpruzp//etfERkZiYCAADz++ONYvHix3aBszpw5iImJwd/+9je0aNECfn5+AIALFy7g2WefRWhoKIKCgvDAAw9g3759dm1/+umnuOOOO+Dn54eWLVti7ty5ttdPURTMmTMHzZo1g8lkQkREBMaNG+fQtt9ou86dO4fBgwejSZMmCAgIQMeOHfGvf/3Lro1///vf6NixI/z9/dGwYUPEx8ejuLgYc+bMwapVq/Dpp5/aDrtdv8+IyLt595++RC504sQJPPLIIxg2bBg++OADHDp0CCNHjoSfn5/dAGXVqlVITk7Gzp07kZGRgWHDhuHuu+/GQw89BIvFgsTERDRr1gw7d+7ExYsXMWnSJOlzRkZG4uOPP8YTTzyB7OxsBAUFwd/f36H+Wq1W9O/fH2FhYdi5cycKCgpU64fKy8uRkJCAuLg4bN++HT4+PnjllVfQp08f7N+/H0aj0eH9M3nyZHzzzTf49NNP0ahRI7z44ovYs2cPYmJibHWef/55/Pe//8WaNWsQERGBTz75BH369MFPP/2E6OhofP/99xg9ejTmz5+Pxx57DF999RVmzZqleq5ffvkFH3/8MdavXw+DwQAAGDBgAPz9/bFx40YEBwfjnXfewYMPPojDhw+jQYMG2L59O4YOHYo333wT99xzD44ePYpRo0YBAFJSUvDxxx9jyZIlWLNmDTp06ACz2awabMncaLtKSkrQtWtXTJ06FUFBQfj888/xzDPPoFWrVujevTtOnTqFwYMHY8GCBXj88cdx8eJFbN++HYqi4IUXXsDBgwdRWFiI9957DwDQoEEDh18XIvICCtEtJCkpSenXr5/wvhdffFFp06aNYrVabWUrVqxQ6tSpo1gsFkVRFKVXr15Kz5497R535513KlOnTlUURVE2btyo+Pj4KKdOnbLdn56ergBQPvnkE0VRFOXYsWMKACUrK0tRFEXZtm2bAkA5f/68XbtRUVHKkiVL7Mo6d+6spKSkKIqiKF9++aXi4+OjnDhxwnb/xo0b7Z7rH//4h2qbSktLFX9/f+XLL78U7gdRfy5evKgYjUblo48+spWdO3dO8ff3V8aPH68oiqL89ttvisFgsOuPoijKgw8+qEyfPl1RFEUZOHCg0rdvX7v7hwwZogQHB9t+TklJUXx9fZXTp0/byrZv364EBQUpJSUldo9t1aqV8s4779ie57XXXrO7/x//+IfSuHFjRVEUZdGiRcptt92mlJWVCbdbxpHtEunbt68yadIkRVEUJTMzUwGgHD9+XFi3qvfle++9Z7d/iMj7cGaJao2DBw8iLi4OOp3OVnb33XejqKgIv//+O5o1awYA6NSpk93jGjdujNOnTwO4sig6MjIS4eHhtvu7d+/utv5GRkYiIiLCVhYXF2dXZ9++ffjll19Qt25du/KSkhIcPXrU4ec6evQoysrKEBsbaytr0KAB2rRpY/v5p59+gsViwW233Wb32NLSUjRs2BDAlf3z+OOP293fvXt3bNiwwa4sKioKoaGhdttRVFRka6fS5cuXbduxb98+fP/993j11Vdt91ssFpSUlODSpUsYMGAAli5dipYtW6JPnz545JFH8Oijj95w7Zgj22WxWPDaa6/ho48+wokTJ1BWVobS0lIEBAQAADp37owHH3wQHTt2REJCAnr37o3/+7//Q/369at8biK6OXCwRHQdX19fu591Oh2sVqvLn0ev10NRFLuy8vJyTW0UFRWha9eu+PDDD1X3XTsYcYWioiIYDAZkZmbaDp1VqlOnjqa2AgMDVW03btxYuJancr1TUVER5s6di/79+6vq+Pn5ITIyEtnZ2fjqq6+Qnp6OP//5z1i4cCG++eYb1WuqdbsWLlyIZcuWYenSpejYsSMCAwMxYcIE2+J0g8GA9PR07NixA5s3b8by5csxY8YM7Ny5Ey1atNCya4jIC3GwRLVGu3bt8PHHH0NRFNvs0vfff4+6deuiadOmDrXRpk0b5ObmIi8vD2FhYQCAH3/8scrHVK4bslgsduWhoaE4deqU7efCwkIcO3bMrr+5ubk4deoUGjduDAD44Ycf7Nq44447sHbtWjRq1AhBQUEObYNIq1at4Ovri507d9pm2M6fP4/Dhw+jV69eAIAuXbrAYrHg9OnTuOeee4TttGnTRrU/brR/KrfDbDbDx8cHzZs3l9bJzs5G69atpe34+/vj0UcfxaOPPooxY8agbdu2+Omnn3DHHXdIH+PIdn3//ffo168fnn76aQBX1pMdPnwY7du3t9XR6XS4++67cffdd2P27NmIiorCJ598guTkZBiNRtXrT0Q3D54NR7ecgoIC7N271+6Wm5uLP//5z8jNzcXYsWNx6NAhfPrpp0hJSUFycjL0esc+Cg899BBatWqFpKQk7N+/H99//z1mzpwJAHaH964VFRUFnU6HDRs24MyZMygqKgIAPPDAA/jHP/6B7du346effkJSUpLdzEZ8fDxuu+02JCUlYd++fdi+fTtmzJhh1/aQIUMQEhKCfv36Yfv27Th27Bi+/vprjBs3Dr///rvD+6xOnToYMWIEJk+ejK1bt+LAgQMYNmyY3X657bbbMGTIEAwdOhTr16/HsWPHsGvXLsybNw+ff/45AGDs2LH44osvsHjxYhw5cgTvvPMONm7cKN03125rXFwcEhMTsXnzZhw/fhw7duzAjBkzsHv3bgDA7Nmz8cEHH2Du3Ln4+eefcfDgQaxZs8a2/99//338/e9/x4EDB/Drr7/in//8J/z9/REVFVXlczuyXdHR0baZo4MHD+JPf/oT8vLybG3s3LkTr732Gnbv3o2cnBysX78eZ86cQbt27QBcOfNx//79yM7OxtmzZzXPIBKRh3l60RSRKyUlJSkAVLcRI0YoiqIoX3/9tXLnnXcqRqNRCQ8PV6ZOnaqUl5fbHt+rVy/bguZK/fr1U5KSkmw/Hzx4ULn77rsVo9GotG3bVvnPf/6jAFA2bdqkKIp6gbeiKMpLL72khIeHKzqdztZWQUGBMnDgQCUoKEiJjIxU3n//fbsF3oqiKNnZ2UrPnj0Vo9Go3HbbbcqmTZvsFngriqKcOnVKGTp0qBISEqKYTCalZcuWysiRI5WCggLhPpItOL948aLy9NNPKwEBAUpYWJiyYMEC1f4oKytTZs+erTRv3lzx9fVVGjdurDz++OPK/v37bXVWrlypNGnSRPH391cSExOVV155RQkPD7fdn5KSonTu3FnVr8LCQmXs2LFKRESE4uvrq0RGRipDhgxRcnJybHU2bdqk9OjRQ/H391eCgoKU7t27KytXrlQURVE++eQTJTY2VgkKClICAwOVu+66S/nqq6+E++B6N9quc+fOKf369VPq1KmjNGrUSJk5c6YydOhQ26Lt//73v0pCQoISGhqqmEwm5bbbblOWL19ua//06dPKQw89pNSpU0cBoGzbts12Hxd4E3k/naJct2iCiDT5/vvv0bNnT/zyyy9o1aqVp7tzQ19//TXuv/9+nD9/vkZCKUeOHIlDhw5h+/btbn+um9H777+PCRMmeF3COxFdxTVLRBp98sknqFOnDqKjo/HLL79g/PjxuPvuu2+KgdK1mjZtikcffVQVruisN954Aw899BACAwOxceNGrFq1Cn/5y19c+hy3ijp16qCiosIWzElE3omDJSKNLl68iKlTpyInJwchISGIj48Xpl17q9jYWBw5cgSA9rPYHLFr1y4sWLAAFy9eRMuWLfHmm2/i2WefdfnzOGr79u14+OGHpfdXriHzhMqLLV9/Fh4ReRcehiOiW9rly5dx4sQJ6f1VnV1HRARwsERERERUJUYHEBEREVWBgyUiIiKiKnCwRERERFQFDpaIiIiIqsDBEhEREVEVOFgiIiIiqgIHS0RERERV+H9+sNewJArYWgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%%time\n", - "h2.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e5ec6a12-4c80-4dc6-99ee-578b3be32b2b", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "45e69e67-a266-4fd0-a0b4-3522cf20252d", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "416d1c7c-5884-467c-a751-a0912a9709c9", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "a0804853-9bed-45b2-823a-837af23771f7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray 'huss' (time: 251288, lat: 128, lon: 256)> Size: 33GB\n",
-       "[8234205184 values with dtype=float32]\n",
-       "Coordinates:\n",
-       "  * time     (time) float64 2MB 6.027e+04 6.027e+04 ... 9.168e+04 9.168e+04\n",
-       "  * lat      (lat) float64 1kB -88.93 -87.54 -86.14 -84.74 ... 86.14 87.54 88.93\n",
-       "  * lon      (lon) float64 2kB 0.0 1.406 2.812 4.219 ... 354.4 355.8 357.2 358.6\n",
-       "Attributes: (12/13)\n",
-       "    description:         This is sampled synoptically.\n",
-       "    history:             none\n",
-       "    online_operation:    instant\n",
-       "    interval_operation:  900 s\n",
-       "    interval_write:      3 h\n",
-       "    _FillValue:          1e+20\n",
-       "    ...                  ...\n",
-       "    standard_name:       specific_humidity\n",
-       "    long_name:           Near-Surface Specific Humidity\n",
-       "    units:               1\n",
-       "    cell_measures:       area: areacella\n",
-       "    coordinates:         height\n",
-       "    cell_methods:        area: mean time: point
" - ], - "text/plain": [ - " Size: 33GB\n", - "[8234205184 values with dtype=float32]\n", - "Coordinates:\n", - " * time (time) float64 2MB 6.027e+04 6.027e+04 ... 9.168e+04 9.168e+04\n", - " * lat (lat) float64 1kB -88.93 -87.54 -86.14 -84.74 ... 86.14 87.54 88.93\n", - " * lon (lon) float64 2kB 0.0 1.406 2.812 4.219 ... 354.4 355.8 357.2 358.6\n", - "Attributes: (12/13)\n", - " description: This is sampled synoptically.\n", - " history: none\n", - " online_operation: instant\n", - " interval_operation: 900 s\n", - " interval_write: 3 h\n", - " _FillValue: 1e+20\n", - " ... ...\n", - " standard_name: specific_humidity\n", - " long_name: Near-Surface Specific Humidity\n", - " units: 1\n", - " cell_measures: area: areacella\n", - " coordinates: height\n", - " cell_methods: area: mean time: point" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ds['huss']" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56582751-861b-4e77-849b-5cd8b165cd87", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cfvenv", - "language": "python", - "name": "cfvenv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/testing/createExampleData.py b/testing/createExampleData.py deleted file mode 100644 index ddd08d2..0000000 --- a/testing/createExampleData.py +++ /dev/null @@ -1,74 +0,0 @@ -import cf -import numpy as np - -def create(i, j, k): - # Create blank field with standard name - - lat_shift = 90*(j-1) - lon_shift = 180*(k-1) - - wrapper = cf.Domain() - - p = cf.Field(properties={ - 'standard_name':'rain' - }) - - shape = (2, 90, 180) - size = np.prod(shape) - - domain_axisT = cf.DomainAxis(shape[0]) - domain_axisLat = cf.DomainAxis(shape[1]) - domain_axisLon = cf.DomainAxis(shape[2]) - - # Create an axis construct for the field with length 3 - time_axis = p.set_construct(domain_axisT) - latitude_axis = p.set_construct(domain_axisLat) - longitude_axis = p.set_construct(domain_axisLon) - - dt = np.random.rand(size).reshape(shape) - - data = cf.Data(dt) - p.set_data(data) - - dimT = cf.DimensionCoordinate( - properties={'standard_name': 'time', - 'units' : 'days since 2018-12-01'}, - data=cf.Data( - [2*i +j +1 for j in range(shape[0])] - ) - ) - - dimLat = cf.DimensionCoordinate( - properties={'standard_name': 'latitude', - 'units' : 'degrees_north'}, - data=cf.Data(np.arange(shape[1])+lat_shift) - ) - - dimLon = cf.DimensionCoordinate( - properties={'standard_name': 'longitude', - 'units' : 'degrees_east'}, - data=cf.Data(np.arange(shape[2])+lon_shift) - ) - - p.set_construct(dimT) - p.set_construct(dimLat) - p.set_construct(dimLon) - - p.nc_set_variable('/rain1/p') - - dimT.nc_set_variable('/rain1/time') - dimLat.nc_set_variable('/rain1/lat') - dimLon.nc_set_variable('/rain1/lon') - - cf.write(p,f'testfiles/raincube/example{i}_{j}_{k}.nc', group=True) - - -for i in range(2): - for j in range(2): - for k in range(2): - create(i, j, k) - - - -g = cf.read('testfiles/raincube/*.nc') -cf.write(g,'testfiles/raincube.nca',cfa=True, group=True) \ No newline at end of file diff --git a/testing/example2.py b/testing/example2.py deleted file mode 100644 index 90f4fcd..0000000 --- a/testing/example2.py +++ /dev/null @@ -1,24 +0,0 @@ -import numpy -import cf - -# Initialise the field construct with properties -Q = cf.Field(properties={'project': 'research', - 'standard_name': 'specific_humidity', - 'units': '1'}) - -# Create the domain axis constructs -domain_axisT = cf.DomainAxis(1) -domain_axisY = cf.DomainAxis(5) -domain_axisX = cf.DomainAxis(8) - -# Insert the domain axis constructs into the field. The -# set_construct method returns the domain axis construct key that -# will be used later to specify which domain axis corresponds to -# which dimension coordinate construct. -axisT = Q.set_construct(domain_axisT) -axisY = Q.set_construct(domain_axisY) -axisX = Q.set_construct(domain_axisX) - -# Create and insert the field construct data -data = cf.Data(numpy.arange(40.).reshape(5, 8)) -Q.set_data(data) \ No newline at end of file diff --git a/testing/kerchunkLazy.py b/testing/kerchunkLazy.py deleted file mode 100644 index c11eb19..0000000 --- a/testing/kerchunkLazy.py +++ /dev/null @@ -1,18 +0,0 @@ -import xarray as xr -import fsspec -kfile = '/gws/nopw/j04/cmip6_prep_vol1/kerchunk-pipeline/complete/CMIP6_rel1_6233/ScenarioMIP_CNRM-CERFACS_CNRM-ESM2-1_ssp119_r1i1p1f2_3hr_huss_gr_v20190328_kr1.0.json' - - -# h1 contains the data array after the slice operation with the below method -#ds = xr.open_dataset(kfile, engine='kerchunk') - -mapper = fsspec.get_mapper('reference://', fo=kfile) -#ds = xr.open_zarr(mapper, consolidated=False) -ds = xr.open_dataset(mapper, engine='zarr', consolidated=False) - - -# Slice operation -h1 = ds['huss'].sel(lat=slice(-60,0), lon=slice(80,180)).isel(time=slice(10000,12000)).mean(dim='time') - -h1_data = h1.data -y = 2 \ No newline at end of file diff --git a/testing/testCfCFARead.py b/testing/testCfCFARead.py deleted file mode 100644 index 0ab05f1..0000000 --- a/testing/testCfCFARead.py +++ /dev/null @@ -1,4 +0,0 @@ -import cf - -f = cf.read('../testfiles/rainmaker.nca') -q = f['p'].to_numpy() \ No newline at end of file diff --git a/testing/testChunkShapes.py b/testing/testChunkShapes.py deleted file mode 100644 index eeb97d6..0000000 --- a/testing/testChunkShapes.py +++ /dev/null @@ -1,71 +0,0 @@ -fragment_shape = (5, 1, 5) -shape = (100, 1, 20) -fragment_dims = (0, 2) -ndim = 3 - -locations = [] - -fragment_sizes = [int(shape[i]/fragment_shape[i]) for i in range(ndim)] -n_fragments = 1 -for i in fragment_shape: - n_fragments *= i - -coord_mixers = [1] -for i in range(ndim-1): - coord_mixers.append(fragment_shape[i]*coord_mixers[i]) - -def find_coords(n): - n += 1 - qs = [] - coord_pointer = ndim - 1 - while coord_pointer >= 0: - q = 0 - while n > coord_mixers[coord_pointer]: - n -= coord_mixers[coord_pointer] - q += 1 - if n < 0: - n += coord_mixers[coord_pointer] - q -= 1 - coord_pointer -= 1 - qs.append(q) - return tuple(reversed(qs)) - - -def build_chunk_set(): - - for n in range(n_fragments): - - coords = find_coords(n) - loc = [] - for x, i in enumerate(coords): - p1 = i*fragment_sizes[x] - p2 = (i+1)*fragment_sizes[x] - loc.append((p1, p2)) - locations.append(loc) - -build_chunk_set() - -chunks = [] - -for dim, (n_fragments, size) in enumerate( - zip(fragment_shape, shape) -): - if dim in fragment_dims: - # This aggregated dimension is spanned by more than - # one fragment. - c = [] - index = [0] * ndim - for j in range(n_fragments): - index[dim] = j - loc = locations[j][dim] - chunk_size = loc[1] - loc[0] - c.append(chunk_size) - - chunks.append(tuple(c)) - else: - # This aggregated dimension is spanned by exactly one - # fragment. Store None, for now, in the expectation - # that it will get overwrittten. - chunks.append(None) - -z = 1 \ No newline at end of file diff --git a/testing/testDomain.py b/testing/testDomain.py deleted file mode 100644 index dce7bf2..0000000 --- a/testing/testDomain.py +++ /dev/null @@ -1,7 +0,0 @@ -import cf - -pwd = '/'.join(__file__.split('/')[:-2]) + '/testfiles' -print(f'Opening files under {pwd}') -f = cf.read(f'{pwd}/huss*') - -print(dir(f)) \ No newline at end of file diff --git a/testing/testKerchunk.py b/testing/testKerchunk.py deleted file mode 100644 index ce307aa..0000000 --- a/testing/testKerchunk.py +++ /dev/null @@ -1,44 +0,0 @@ -import json - -filename = '/gws/nopw/j04/cmip6_prep_vol1/kerchunk-pipeline/complete/CMIP6_rel1_6233/ScenarioMIP_CNRM-CERFACS_CNRM-ESM2-1_ssp119_r1i1p1f2_3hr_huss_gr_v20190328_kr1.0.json' - -with open(filename) as f: - refs=json.load(f) - -huss_chunk_info = {} - -for i in range(58430,58451): - huss_chunk_info[f'huss/{i}.0.0'] = None - -for r in refs['refs'].keys(): - if r in huss_chunk_info: - huss_chunk_info[r] = refs['refs'][r] - -for h in list(huss_chunk_info.keys()): - print(h, huss_chunk_info[h]) - -""" -Outputs: - -huss/58430.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641249631, 96024] -huss/58431.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641345655, 96058] -huss/58432.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641441713, 95985] -huss/58433.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641540834, 95962] -huss/58434.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641636796, 95997] -huss/58435.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641732793, 96002] -huss/58436.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641828795, 96054] -huss/58437.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5641924849, 96023] -huss/58438.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5642020872, 95962] -huss/58439.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', 5642116834, 96067] -huss/58440.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 32776, 95925] -huss/58441.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 128701, 95847] -huss/58442.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 224548, 96041] -huss/58443.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 320589, 95997] -huss/58444.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 416586, 96109] -huss/58445.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 512695, 96052] -huss/58446.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 608747, 96037] -huss/58447.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 704784, 96048] -huss/58448.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 800832, 95971] -huss/58449.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 896803, 96086] -huss/58450.0.0 ['/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', 992889, 96187] -""" \ No newline at end of file diff --git a/testing/testLocal.ipynb b/testing/testLocal.ipynb deleted file mode 100644 index 59d7149..0000000 --- a/testing/testLocal.ipynb +++ /dev/null @@ -1,127 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "108657e7-b5d1-4808-8b77-959b29ac6509", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Dan's local copy of cf-python\n", - "Dan's cfdm copy\n" - ] - } - ], - "source": [ - "from cf_python import cf\n", - "from cfdm_local import cfdm" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "483e9370-768e-49df-98e8-1e280d0f8b06", - "metadata": {}, - "outputs": [], - "source": [ - "x = cf.example_field(0)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9c3880eb-e210-4f04-8057-c1f11042cd8f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "31620f82-1773-4e55-8ea0-68bf529fb097", - "metadata": {}, - "outputs": [], - "source": [ - "d = x.get_data()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "dc188eea-c142-4c27-bfbf-9de6b95cec63", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "d.cfa_get_write()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af59449c-2fc5-4107-9c4d-f338a4004dc7", - "metadata": {}, - "outputs": [], - "source": [ - "cf.write(x[:2], 'file1.nc')\n", - "cf.write(x[2:], 'file2.nc')\n", - "f = cf.read('file[12].nc')[0]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "01f6733a-5c9b-44d6-becf-1d731e2408fb", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 + Jaspy", - "language": "python", - "name": "jaspy" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/testing/testPerformance.ipynb b/testing/testPerformance.ipynb deleted file mode 100644 index 4f07412..0000000 --- a/testing/testPerformance.ipynb +++ /dev/null @@ -1,1375 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "6a13c075-4abb-4cfa-82d1-3657d4a2cc8c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 3.12 s, sys: 1.5 s, total: 4.63 s\n", - "Wall time: 7.23 s\n" - ] - } - ], - "source": [ - "%%time \n", - "import xarray as xr\n", - "kfile = '/gws/nopw/j04/cmip6_prep_vol1/kerchunk-pipeline/complete/CMIP6_rel1_6233/ScenarioMIP_CNRM-CERFACS_CNRM-ESM2-1_ssp119_r1i1p1f2_3hr_huss_gr_v20190328_kr1.0.json'\n", - "ds = xr.open_dataset(kfile, engine='kerchunk')" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c57d0ac5-1344-4421-8f37-4043758557f2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 1.16 s, sys: 88.8 ms, total: 1.25 s\n", - "Wall time: 1.28 s\n" - ] - } - ], - "source": [ - "%%time\n", - "h1 = ds['huss'].sel(lat=slice(-60,0), lon=slice(80,180))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "5d61c566-f15f-445f-bc98-da2734273657", - "metadata": {}, - "outputs": [], - "source": [ - "h2 = h1.isel(time=slice(0,10))" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "648a352f-bf43-4562-bd67-41ae070e7a80", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray 'huss' (time: 10, lat: 43, lon: 72)> Size: 124kB\n",
-       "dask.array<getitem, shape=(10, 43, 72), dtype=float32, chunksize=(1, 43, 72), chunktype=numpy.ndarray>\n",
-       "Coordinates:\n",
-       "    height   float64 8B ...\n",
-       "  * lat      (lat) float64 344B -59.53 -58.13 -56.73 ... -3.502 -2.101 -0.7004\n",
-       "  * lon      (lon) float64 576B 80.16 81.56 82.97 84.38 ... 177.2 178.6 180.0\n",
-       "  * time     (time) datetime64[ns] 80B 2015-01-01T03:00:00 ... 2015-01-02T06:...\n",
-       "Attributes:\n",
-       "    cell_measures:       area: areacella\n",
-       "    cell_methods:        area: mean time: point\n",
-       "    description:         This is sampled synoptically.\n",
-       "    history:             none\n",
-       "    interval_operation:  900 s\n",
-       "    interval_write:      3 h\n",
-       "    long_name:           Near-Surface Specific Humidity\n",
-       "    online_operation:    instant\n",
-       "    standard_name:       specific_humidity\n",
-       "    units:               1
" - ], - "text/plain": [ - " Size: 124kB\n", - "dask.array\n", - "Coordinates:\n", - " height float64 8B ...\n", - " * lat (lat) float64 344B -59.53 -58.13 -56.73 ... -3.502 -2.101 -0.7004\n", - " * lon (lon) float64 576B 80.16 81.56 82.97 84.38 ... 177.2 178.6 180.0\n", - " * time (time) datetime64[ns] 80B 2015-01-01T03:00:00 ... 2015-01-02T06:...\n", - "Attributes:\n", - " cell_measures: area: areacella\n", - " cell_methods: area: mean time: point\n", - " description: This is sampled synoptically.\n", - " history: none\n", - " interval_operation: 900 s\n", - " interval_write: 3 h\n", - " long_name: Near-Surface Specific Humidity\n", - " online_operation: instant\n", - " standard_name: specific_humidity\n", - " units: 1" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "h2" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "aeb3bd84-761f-48d1-9487-31b6a3d6f9ed", - "metadata": {}, - "outputs": [], - "source": [ - "h3 = h2.mean(dim='time')" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "c7a6cada-b095-448d-9af6-edeec73627d4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray 'huss' (time: 10, lat: 43, lon: 72)> Size: 124kB\n",
-       "dask.array<getitem, shape=(10, 43, 72), dtype=float32, chunksize=(1, 43, 72), chunktype=numpy.ndarray>\n",
-       "Coordinates:\n",
-       "    height   float64 8B ...\n",
-       "  * lat      (lat) float64 344B -59.53 -58.13 -56.73 ... -3.502 -2.101 -0.7004\n",
-       "  * lon      (lon) float64 576B 80.16 81.56 82.97 84.38 ... 177.2 178.6 180.0\n",
-       "  * time     (time) datetime64[ns] 80B 2015-01-01T03:00:00 ... 2015-01-02T06:...\n",
-       "Attributes:\n",
-       "    cell_measures:       area: areacella\n",
-       "    cell_methods:        area: mean time: point\n",
-       "    description:         This is sampled synoptically.\n",
-       "    history:             none\n",
-       "    interval_operation:  900 s\n",
-       "    interval_write:      3 h\n",
-       "    long_name:           Near-Surface Specific Humidity\n",
-       "    online_operation:    instant\n",
-       "    standard_name:       specific_humidity\n",
-       "    units:               1
" - ], - "text/plain": [ - " Size: 124kB\n", - "dask.array\n", - "Coordinates:\n", - " height float64 8B ...\n", - " * lat (lat) float64 344B -59.53 -58.13 -56.73 ... -3.502 -2.101 -0.7004\n", - " * lon (lon) float64 576B 80.16 81.56 82.97 84.38 ... 177.2 178.6 180.0\n", - " * time (time) datetime64[ns] 80B 2015-01-01T03:00:00 ... 2015-01-02T06:...\n", - "Attributes:\n", - " cell_measures: area: areacella\n", - " cell_methods: area: mean time: point\n", - " description: This is sampled synoptically.\n", - " history: none\n", - " interval_operation: 900 s\n", - " interval_write: 3 h\n", - " long_name: Near-Surface Specific Humidity\n", - " online_operation: instant\n", - " standard_name: specific_humidity\n", - " units: 1" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "h2" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bd221950-8cb6-4718-85c9-0d20cc116d59", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 3.5 s, sys: 1.42 s, total: 4.92 s\n", - "Wall time: 5.63 s\n" - ] - } - ], - "source": [ - "%%time\n", - "import fsspec\n", - "import xarray as xr\n", - "\n", - "kfile = '/gws/nopw/j04/cmip6_prep_vol1/kerchunk-pipeline/complete/CMIP6_rel1_6233/ScenarioMIP_CNRM-CERFACS_CNRM-ESM2-1_ssp119_r1i1p1f2_3hr_huss_gr_v20190328_kr1.0.json'\n", - "mapper = fsspec.get_mapper('reference://',fo=kfile)\n", - "ds = xr.open_zarr(mapper, consolidated=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "ca1d959e-9c03-48e1-a0b4-7f0c7a39324a", - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'ds' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "File \u001b[0;32m:1\u001b[0m\n", - "\u001b[0;31mNameError\u001b[0m: name 'ds' is not defined" - ] - } - ], - "source": [ - "%%time\n", - "h1 = ds['huss'].sel(lat=slice(-60,0), lon=slice(80,180)).isel(time=slice(10000,12000)).mean(dim='time')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b0e2f0be-6a13-45c5-a775-773066bc1811", - "metadata": {}, - "outputs": [], - "source": [ - "h1" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "6c4200b8-3a06-4c71-8d51-f39204da3058", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 752 μs, sys: 0 ns, total: 752 μs\n", - "Wall time: 776 μs\n" - ] - } - ], - "source": [ - "%%time\n", - "h2 = h1.compute()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "312f84a9-5b4f-44f7-886b-cc2d9b8528ab", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 458 ms, sys: 57.7 ms, total: 516 ms\n", - "Wall time: 984 ms\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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V2lk++9mrzERVWwcLBE5qUGDBlAUBfXq1cMdd9yh6bGRkZHYunUrrly5Yje7dOzYsTL35/pBhiPS09NRr149h+pu3brVofyisWPHYsmSJViwYAF69erlcF9iYmLwwQcf4NChQ2jSpImtfMeOHbb7iYjKG6MDiJzUrVs3GAwGzJw5UzUToSgKLly4IH1sQkICioqK8P7779vKrFYrFi1aVOb+lAy6Ll265FD9kjVLjtwcWbM0d+5cvPHGG5g0aRJGjhwprZednY3Dhw8jOzvbVtalSxf4+vriX//6l61MURQkJSWhVq1aaNu2rUPbRETkSpxZInJSgwYN8Morr2DixIk4efIkunbtisqVK+PEiRNYu3YthgwZIr30RteuXdGmTRuMGTMGx44dQ3R0NL788kvbqfxaZ4mAa+t6mjRpglWrVuGOO+5AtWrVcOedd+LOO+8U1nflmqW1a9di3LhxiIqKQuPGjfHJJ5/Y3f/www8jNDTUVnfgwIFYsmQJBgwYAACoXbs2Ro0ahblz56KoqAh333031q1bh23btmH58uUMpCQij+BgicgFJkyYgDvuuANvvvkmZs6cCeDaouGOHTviiSeekD7OYDDgq6++wsiRI7Fs2TLo9Xo8+eSTmD59Ou69917bWWBaffDBBxg+fDhGjx6NwsJCTJ8+XTpYcqX9+/cDAI4ePYq+ffuq7t+6dattsCTz+uuvo2rVqnjvvfewdOlSREVF4ZNPPsEzzzzjlj4TEd2MTuEKRiKvs27dOjz55JP48ccfce+993q6O7eMDh06oKioCF988QWMRiMCAwNd1valS5dQXFyMu+66C82bN8f69etd1jYReTeuWSLysBuzgywWCxYuXIjAwEDcddddHurVrWv79u0ICQlx+UxUhw4dEBISorqEDRHd/ngYjsjDhg8fjqtXryIuLg4FBQVYs2YNtm/fjtdee83pXKGKZt68ebZspBvzqJz13nvv2a5t5+q2ici78TAckYetWLEC8+bNw7Fjx5Cfn4+GDRvi+eefxwsvvODprhEREW7TwdKiRYswd+5cZGRkoEWLFli4cKHtQpxEREREWtx2a5ZWrVqFxMRETJ8+HXv27EGLFi2QkJCAs2fPerprREREdAu67WaWYmNjcffdd+Odd94BcC3gLyIiAsOHD8eECRM83DsiIiK61dxWC7wLCwuxe/du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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "%%time\n", - "h2.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14d9abf7-f490-415e-8090-acf3c270f4dd", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "40540ddd-0129-4bd2-84b3-9eaba4697c4a", - "metadata": {}, - "outputs": [], - "source": [ - "ds['huss'].sel(lat=slice(-60,0), lon=slice(80,180)).isel(time=slice(10000,12000))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "45c554c8-5f2e-4758-b920-c93058adecee", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cfvenv", - "language": "python", - "name": "cfvenv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/testing/testPlainAgg.py b/testing/testPlainAgg.py deleted file mode 100644 index 93e131f..0000000 --- a/testing/testPlainAgg.py +++ /dev/null @@ -1,15 +0,0 @@ -import cf - -files = [ - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_205501010300-207501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_207501010300-209501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_209501010300-210101010000.nc' -] - -filename='testfiles/ScenarioMIP_CNRM-CERFACS_CNRM-ESM2-1_ssp119_r1i1p1f2_3hr_huss_gr_CFA1.1.nc' - -g = cf.read(files) - -cf.write(g, filename, cfa=True) \ No newline at end of file diff --git a/testing/testRainCube.ipynb b/testing/testRainCube.ipynb deleted file mode 100644 index 8967d17..0000000 --- a/testing/testRainCube.ipynb +++ /dev/null @@ -1,256 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 5, - "id": "b66e2b18-f8a2-48a8-a2dc-cc8c6eafe246", - "metadata": {}, - "outputs": [], - "source": [ - "import xarray as xr" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "f9a2bf35-3f05-4023-910c-c94b37fd2ce1", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "2e6afa19-a568-4b95-b449-917b5b8a2e6e", - "metadata": {}, - "outputs": [], - "source": [ - "ds = xr.open_dataset('../testfiles/raincube.nca', engine='CFA', group='rain1',\n", - " cfa_options={'substitutions':\"cfa_python_dw:CFAPyX\", \"decode_cfa\":True})" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "e9fdc8a8-f56b-4826-aac6-1d21d82c391f", - "metadata": {}, - "outputs": [], - "source": [ - "p = ds['p'].sel(time=slice(1,2), latitude=slice(50,54), longitude=slice(0,9)).mean(dim='time')" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "51025021-cc36-4927-98fe-291309bae6c3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "p.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "3d839b9a-76ad-463b-b12d-596e1dae45e9", - "metadata": {}, - "outputs": [], - "source": [ - "import netCDF4" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "b73d5ef5-f813-4ef4-9fc6-59b0f014814c", - "metadata": {}, - "outputs": [], - "source": [ - "ref = netCDF4.Dataset('../testfiles/raincube/example0_1_1.nc')" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "08346332-8b57-4752-bd49-3fff03a29bbb", - "metadata": {}, - "outputs": [], - "source": [ - "pr = ref.groups['rain1'].variables['p']" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "2629efe0-f2bb-4135-bb4d-8730e3483894", - "metadata": {}, - "outputs": [], - "source": [ - "prn = np.array(pr)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "9b34e9ba-1684-413c-bfe6-2152b4ef824a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(90, 180)" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "prn[0].shape" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "a2a400be-e58a-4bde-b65a-130e04e97074", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(90, 180)" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "p[0].shape" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "d0b08cbe-de19-451a-8ec6-74eec30c2f73", - "metadata": {}, - "outputs": [], - "source": [ - "xfer = p[0] - prn[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "cdea688a-5a8c-46cf-9eab-cae5d318f2ae", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([[0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " ...,\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.],\n", - " [0., 0., 0., ..., 0., 0., 0.]])" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "xfer" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "8d83c675-321b-4ffb-a462-8e3f9ce6f408", - "metadata": {}, - "outputs": [], - "source": [ - "qac = pn[140:144]" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "779c09f2-56b6-4337-96d0-0e08d31f03b0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([0.41235605, 0.3201409 , 0.44941687, 0.62288974, 0.55505699,\n", - " 0.43156043, 0.2503939 , 0.58497592, 0.57973211])" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qac[0][180:189]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f5dad19a-b170-4746-af30-349bfc3690af", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "cfvenv", - "language": "python", - "name": "cfvenv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/testing/testXAgg.py b/testing/testXAgg.py deleted file mode 100644 index 09bcdee..0000000 --- a/testing/testXAgg.py +++ /dev/null @@ -1,10 +0,0 @@ -import xarray as xr - -loc = '/'.join(__file__.split('/')[:-2]) - -ds = xr.open_dataset(f'{loc}/testfiles/raincube.nca', engine='CFA', group='rain1', - cfa_options={'substitutions':"cfa_python_dw:CFAPyX", "decode_cfa":True}) - -p = ds['p'].sel(time=slice(1,3),latitude=slice(50,54), longitude=slice(0,9)) -pq = p.mean(dim='time') -pq.plot() \ No newline at end of file diff --git a/testing/trackCf.py b/testing/trackCf.py deleted file mode 100644 index ba112f2..0000000 --- a/testing/trackCf.py +++ /dev/null @@ -1,14 +0,0 @@ -import cf - -files = [ - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_201501010300-203501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_203501010300-205501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_205501010300-207501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_207501010300-209501010000.nc', - '/badc/cmip6/data/CMIP6/ScenarioMIP/CNRM-CERFACS/CNRM-ESM2-1/ssp119/r1i1p1f2/3hr/huss/gr/v20190328/huss_3hr_CNRM-ESM2-1_ssp119_r1i1p1f2_gr_209501010300-210101010000.nc' -] - -filename='ScenarioMIP_CNRM-CERFACS_CNRM-ESM2-1_ssp119_r1i1p1f2_3hr_huss_gr_CFA1.1.nc' - -g = cf.read(files, chunks=None) -cf.write(g, filename, cfa=True) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/rain/example0.nc b/tests/rain/example0.nc new file mode 100644 index 0000000..a86e26b Binary files /dev/null and b/tests/rain/example0.nc differ diff --git a/tests/rain/example1.nc b/tests/rain/example1.nc new file mode 100644 index 0000000..9f8b13e Binary files /dev/null and b/tests/rain/example1.nc differ diff --git a/tests/rain/example2.nc b/tests/rain/example2.nc new file mode 100644 index 0000000..7c62683 Binary files /dev/null and b/tests/rain/example2.nc differ diff --git a/tests/rain/example3.nc b/tests/rain/example3.nc new file mode 100644 index 0000000..5b25382 Binary files /dev/null and b/tests/rain/example3.nc differ diff --git a/tests/rain/example4.nc b/tests/rain/example4.nc new file mode 100644 index 0000000..a0539f9 Binary files /dev/null and b/tests/rain/example4.nc differ diff --git a/tests/rain/example5.nc b/tests/rain/example5.nc new file mode 100644 index 0000000..15b3503 Binary files /dev/null and b/tests/rain/example5.nc differ diff --git a/tests/rain/example6.nc b/tests/rain/example6.nc new file mode 100644 index 0000000..360ddf1 Binary files /dev/null and b/tests/rain/example6.nc differ diff --git a/tests/rain/example7.nc b/tests/rain/example7.nc new file mode 100644 index 0000000..cc845c1 Binary files /dev/null and b/tests/rain/example7.nc differ diff --git a/tests/rain/example8.nc b/tests/rain/example8.nc new file mode 100644 index 0000000..0ee4643 Binary files /dev/null and b/tests/rain/example8.nc differ diff --git a/tests/rain/example9.nc b/tests/rain/example9.nc new file mode 100644 index 0000000..e6860f9 Binary files /dev/null and b/tests/rain/example9.nc differ diff --git a/tests/rain/rainmaker.nca b/tests/rain/rainmaker.nca new file mode 100644 index 0000000..06a3425 Binary files /dev/null and b/tests/rain/rainmaker.nca differ diff --git a/tests/test_cfa.py b/tests/test_cfa.py new file mode 100644 index 0000000..0ad857b --- /dev/null +++ b/tests/test_cfa.py @@ -0,0 +1,31 @@ +# All routines for testing CFA general methods. +import xarray as xr + +def test_cfa_pure(): + + ds = xr.open_dataset('tests/rain/rainmaker.nca', engine='CFA', + cfa_options={'substitutions':"/home/users/dwest77/Documents/cfa_python_dw/testfiles/:tests/"}) + + ## Test global dataset + assert not hasattr(ds,'address') + assert not hasattr(ds,'shape') + assert not hasattr(ds,'location') + + assert 'p' in ds + assert ds['p'].shape == (20, 180, 360) + + p_sel = ds['p'].sel(time=slice(1,3),latitude=slice(50,54), longitude=slice(0,9)) + + assert p_sel.shape == (3, 5, 10) + assert not hasattr(p_sel, 'aggregated_data') + assert not hasattr(p_sel, 'aggregated_dimensions') + + p_mean = p_sel.mean(dim='time') + + assert p_mean.shape == (5, 10) + assert (p_mean[0][0].to_numpy() - 0.683402) < 0.01 + + p_value = p_sel.mean() + + assert p_value.shape == () + assert (p_value.to_numpy() - 0.53279) < 0.01 \ No newline at end of file