#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
xarray and pandas xoa accessors
"""
# Copyright 2020-2021 Shom
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import warnings
from .misc import ERRORS
class _BasicCFAccessor_(object):
def __init__(self, obj, cfspecs=None):
from . import cf
if cfspecs is None:
cfspecs = cf.infer_cf_specs(obj)
self._obj = obj
self.set_cf_specs(cfspecs)
def _assign_cf_specs_(self):
from . import cf
if self._cfspecs.name:
self._obj = cf.assign_cf_specs(self._obj, self._cfspecs, register=True)
def set_cf_specs(self, cfspecs):
"""Set the internal :class:`~xoa.cf.CFSpecs` used by this accessor
If the specs object has a :cfsec:`registration name <register>`,
it is assigned to
the current object and its children with function
:func:`~xoa.cf.assign_cf_specs`. This set the "cfspecs" encoding
to the name.
Parameters
----------
cfspecs: xoa.cf.CFSpecs
See also
--------
:ref:`uses.cf`
"""
from . import cf
assert isinstance(cfspecs, cf.CFSpecs)
self._cfspecs = cfspecs
self._assign_cf_specs_()
def get_cf_specs(self):
"""Get the internal :class:`~xoa.cf.CFSpecs` instance used by this accessor
If not provided at the initialization, it is infered with :func:`xoa.cf.infer_cf_specs`.
Return
------
xoa.cf.CFSpecs
See also
--------
:ref:`uses.cf`
"""
return self._cfspecs
cfspecs = property(fget=get_cf_specs, fset=set_cf_specs, doc="The CFSpecs instance")
class _CFAccessor_(_BasicCFAccessor_):
_search_category = None
def __init__(self, obj, cfspecs=None):
_BasicCFAccessor_.__init__(self, obj, cfspecs)
self._coords = None
self._data_vars = None
self._cache = {}
self._obj = self.infer_coords()
self._assign_cf_specs_()
@ERRORS.format_method_docstring
def get(self, cf_name, loc="any", single=True, errors="ignore"):
"""Search for a data array or coordinate knowing its generic CF name
Search is made with the :meth:`~xoa.cf.CFSpecs.search` method of the
:meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Parameters
----------
name: str
Generic CF name
loc: str, {{"any", None}}, {{"", False}}
- str: one of these locations
- None or "any": any
- False or "": no location
{errors}
See also
--------
:meth:`xoa.cf.CFSpecs.search`
:meth:`xoa.cf.CFCoordSpecs.search`
:meth:`xoa.cf.CFVarSpecs.search`
"""
kwargs = dict(cf_name=cf_name, loc=loc, get="obj", single=single, errors=errors)
if self._search_category is None:
return self._cfspecs.search(self._obj, **kwargs)
return self._cfspecs[self._search_category].search(self._obj, **kwargs)
@ERRORS.format_method_docstring
def get_coord(self, cf_name, loc="any", single=True, errors="ignore"):
"""Search for a coordinate knowing its generic CF name
Search is made with the :meth:`~xoa.cf.CFSpecs.search_coord` method of the
:meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Parameters
----------
cf_name: str
Generic CF name
loc: str, {{"any", None}}, {{"", False}}
- str: one of these locations
- None or "any": any
- False or "": no location
{errors}
See also
--------
:meth:`xoa.cf.CFSpecs.search`
:meth:`xoa.cf.CFCoordSpecs.search`
:meth:`xoa.cf.CFVarSpecs.search`
"""
return self._cfspecs.search_coord(
self._obj, cf_name=cf_name, loc=loc, get="obj", single=single, errors="ignore"
)
def __getattr__(self, cf_name):
"""Shortcut to :meth:`get`"""
return self.get(cf_name, errors="ignore")
def __getitem__(self, cf_name):
"""Shortcut to :meth:`get`"""
return self.get(cf_name, errors="ignore")
def auto_format(self, **kwargs):
"""Rename variables and coordinates and fill their attributes
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Return
------
xarray.Dataset, xarray.DataArray
See also
--------
:meth:`xoa.cf.CFSpecs.auto_format`
:ref:`uses.cf`
"""
return self._cfspecs.auto_format(self._obj, **kwargs)
def fill_attrs(self, **kwargs):
"""Fill missing attributes
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Return
------
xarray.Dataset, xarray.DataArray
See also
--------
:meth:`xoa.cf.CFSpecs.fill_attrs`
:ref:`uses.cf`
"""
return self._cfspecs.fill_attrs(self._obj, **kwargs)
def to_loc(self, **locs):
"""Set the staggered grid location for specified names
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Parameters
----------
locs: dict
**Keys are root names**, values are new locations.
A value of `False`, remove the location.
A value of `None` left it as is.
See also
--------
:meth:`xoa.cf.CFSpecs.to_loc`
reloc
"""
return self._cfspecs.to_loc(self._obj, **locs)
def reloc(self, **locs):
"""Convert given staggered grid locations to other locations
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Parameters
----------
obj: xarray.Dataset, xarray.DataArray
locs: dict
**Keys are locations**, values are new locations.
A value of `False` or `None`, remove the location.
See also
--------
:meth:`xoa.cf.CFSpecs.reloc`
to_loc
"""
return self._cfspecs.reloc(self._obj, **locs)
def infer_coords(self, **kwargs):
"""Infer coordinates and set them as coordinates
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Return
------
xarray.Dataset, xarray.DataArray
See also
--------
:meth:`xoa.cf.CFSpecs.infer_coords`
:ref:`uses.cf`
"""
return self.cfspecs.infer_coords(self._obj, **kwargs)
def decode(self, **kwargs):
"""Rename variables and coordinates to generic names
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
Return
------
xarray.Dataset, xarray.DataArray
See also
--------
:meth:`xoa.cf.CFSpecs.decode`
:meth:`xoa.cf.CFSpecs.encode`
:ref:`uses.cf`
"""
return self.cfspecs.decode(self._obj, **kwargs)
def encode(self, **kwargs):
"""Rename variables and coordinates to specialized names
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
Return
------
xarray.Dataset, xarray.DataArray
See also
--------
:meth:`xoa.cf.CFSpecs.encode`
:meth:`xoa.cf.CFSpecs.decode`
:ref:`uses.cf`
"""
return self.cfspecs.encode(self._obj, **kwargs)
@ERRORS.format_method_docstring
def get_depth(self, errors="ignore"):
"""Get the depth as computed or recognized by the :meth:`~xoa.cf.CFSpecs`
If a depth variable cannot be found, it tries to compute either
from sigma-like coordinates or from layer thinknesses.
Parameters
----------
{errors}
Return
------
xarray.DataArray, None
See also
--------
:func:`xoa.coords.get_depth`
:func:`xoa.grid.decode_cf_dz2depth`
:func:`xoa.sigma.decode_cf_sigma`
:ref:`uses.cf`
"""
from .coords import get_depth
return get_depth(self._obj, errors=errors)
@property
def coords(self):
"""Sub-accessor for coords only"""
if self._coords is None:
self._coords = _CoordAccessor_(self._obj, self._cfspecs)
return self._coords
@property
def data_vars(self):
"""Sub-accessor for data_vars only"""
if self._data_vars is None:
self._data_vars = _DataVarAccessor__(self._obj, self._cfspecs)
return self._data_vars
class _CoordAccessor_(_CFAccessor_):
_search_category = 'coords'
@property
def dim(self):
from .cf import XoaError
try:
return self._cfspecs.coords.search_dim(self._obj)[0]
except XoaError:
return
def get_dim(self, cf_arg, loc="any"):
"""Get a dimension name knowing its type or generic CF name
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
Parameters
----------
cf_arg: None, {{"x", "y", "z", "t", "f"}}
Dimension type
loc:
Location
Return
------
str, None
Dimension name or None of not found
See also
--------
:meth:`xoa.cf.CFSpecs.search_dim`
"""
cf_arg = cf_arg.lower()
if not hasattr(self, '_dims'):
self._dims = {}
if cf_arg not in self._dims:
self._dims[cf_arg] = self._cfspecs.coords.search_dim(self._obj, cf_arg, loc=loc)
return self._dims[cf_arg]
@property
def xdim(self):
"""X dimension
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
-------
:meth:`xoa.cf.CFSpecs.search_dim`
"""
return self.get_dim("x")
@property
def ydim(self):
"""Y dimension name
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
-------
:meth:`xoa.cf.CFSpecs.search_dim`
"""
return self.get_dim("y")
@property
def zdim(self):
"""Z dimension name
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
-------
:meth:`xoa.cf.CFSpecs.search_dim`
"""
return self.get_dim("z")
@property
def tdim(self):
"""T (time) dimension name
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
-------
:meth:`xoa.cf.CFSpecs.search_dim`
"""
return self.get_dim("t")
@property
def fdim(self):
"""F (forecast) dimension name
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
-------
:meth:`xoa.cf.CFSpecs.search_dim`
"""
return self.get_dim("f")
class _DataVarAccessor__(_CFAccessor_):
_search_category = "data_vars"
[docs]
class CFDatasetAccessor(_CFAccessor_):
@property
def ds(self):
return self._obj
[docs]
class CFDataArrayAccessor(_CoordAccessor_):
@property
def da(self):
return self._obj
@property
def cf_name(self):
"""Get the generic name that matches this array
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
--------
:meth:`xoa.cf.CFSpecs.match`
"""
if 'name' not in self._cache:
category, name = self._cfspecs.match(self._obj)
self._cache["category"] = category
self._cache["name"] = name
return self._cache["name"]
name = cf_name
@property
def attrs(self):
"""Get the generic attributes that matches this array
This makes use of the :meth:`~xoa.cf.CFSpecs` instance was set at initialisation or
inferred with :func:`xoa.cf.infer_cf_specs`.
If no specialized name is declared in the specs, generic names are used.
See also
--------
:meth:`xoa.cf.CFSpecs.get_attrs`
"""
if "attrs" not in self._cache:
if self.name:
cf_attrs = self._cfspecs[self._cache["category"]].get_attrs(
self._cache["name"], multi=True
)
self._cache["attrs"] = self._cfspecs.sglocator.patch_attrs(
self._obj.attrs, cf_attrs
)
else:
self._cache["attrs"] = {}
return self._cache["attrs"]
def __getattr__(self, attr):
if self.name and self.attrs and attr in self.attrs:
return self._cache["attrs"][attr]
return _CoordAccessor_.__getattr__(self, attr)
[docs]
class SigmaAccessor(_BasicCFAccessor_):
"""Dataset accessor to compute depths from sigma-like coordinates
This follows the CF cnventions.
Example
-------
>>> ds = xr.open_dataset('croco.nc')
>>> ds = ds.decode_sigma()
"""
[docs]
def __init__(self, ds, cfspecs=None):
assert hasattr(ds, "data_vars"), "ds must be a xarray.Dataset"
_BasicCFAccessor_.__init__(self, ds, cfspecs)
self._ds = self._obj
[docs]
@ERRORS.format_method_docstring
def decode(self, rename=False, errors="raise"):
"""Compute depth from sigma coordinates
Parameters
----------
rename: bool
Rename and format arrays ot make them compliant with
:mod:`xoa.cf`
{errors}
Return
------
xarray.Dataset
See also
--------
:func:`xoa.sigma.decode_cf_sigma`
"""
from .sigma import decode_cf_sigma
return decode_cf_sigma(self._ds, rename=rename, errors=errors)
def __call__(self):
"""Shortcut to :meth:`decode`"""
return self.decode()
[docs]
def get_sigma_terms(self, loc=None, rename=False):
"""Call :func:`get_sigma_terms` on the dataset
It operates like this:
1. Search for the sigma variables.
2. Parse their ``formula_terms`` attribute.
3. Create a dict for each locations from names in datasets to
:mod:`xoa.cf` compliant names that are also used in conversion
functions.
Parameters
----------
ds: xarray.Dataset
loc: str, {"any", None}
Staggered grid location.
If any or None, results for all locations are returned.
Returns
-------
dict, dict of dict
A dict is generated for a given sigma variable,
whose keys are array names, like ``"sc_r"``,
and values are :mod:`~xoa.cf` names, like ``"sig"``.
A special key is the ``"type"`` whose corresponding value
is the ``standard_name``, stripped from its potential staggered grid
location indicator.
If ``loc`` is ``"any"`` or ``None``,
each dict is embedded in a master dict
whose keys are staggered grid location. If no location is found,
the key is set ``None``.
Raises
------
xoa.sigma.XoaSigmaError
In case of:
- inconsistent staggered grid location in dataarrays
as checked by :meth:`xoa.cf.SGLocator.get_location`
- no standard_name in sigma/s variable
- a malformed formula
- a formula term variable that is not found in the dataset
- an unknown formula term name
See also
--------
:func:`xoa.sigma.get_sigma_terms`
"""
from .sigma import get_sigma_terms
return get_sigma_terms(self._ds, loc=loc, rename=rename)
[docs]
class XoaDataArrayAccessor(CFDataArrayAccessor):
@property
def cf(self):
"""The :class:`CFDataArrayAccessor` subaccessor"""
if not hasattr(self, "_cf"):
self._cf = CFDataArrayAccessor(self._ds, self._cfspecs)
return self._cf
[docs]
class XoaDatasetAccessor(CFDatasetAccessor):
@property
def cf(self):
"""The :class:`~xoa.accessors.CFDatasetAccessor` subaccessor"""
if not hasattr(self, "_cf"):
self._cf = CFDatasetAccessor(self._ds, self._cfspecs)
return self._cf
@property
def decode_sigma(self):
"""The :class:`~xoa.accessors.SigmaAccessor` subaccessor for sigma coordinates"""
if not hasattr(self, "_sigma"):
self._sigma = SigmaAccessor(self._ds)
return self._sigma
def _register_xarray_accessors_(dataarrays=None, datasets=None):
"""Silently register xarray accessors"""
import xarray as xr
with warnings.catch_warnings():
warnings.simplefilter("ignore", xr.core.extensions.AccessorRegistrationWarning)
if dataarrays:
for name, cls in dataarrays.items():
xr.register_dataarray_accessor(name)(cls)
if datasets:
for name, cls in datasets.items():
xr.register_dataset_accessor(name)(cls)
[docs]
def register_cf_accessors(name='xcf'):
"""Register the cf accessors"""
_register_xarray_accessors_(
dataarrays={name: CFDataArrayAccessor},
datasets={name: CFDatasetAccessor},
)
[docs]
def register_sigma_accessor(name='decode_sigma'):
"""Register the sigma decoding accessor"""
_register_xarray_accessors_(datasets={name: SigmaAccessor})
[docs]
def register_xoa_accessors(name='xoa'):
"""Register the main xoa accessors"""
_register_xarray_accessors_(
dataarrays={name: XoaDataArrayAccessor},
datasets={name: XoaDatasetAccessor},
)