Source code for metpy.io.cdm

# Copyright (c) 2015,2016 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
r"""Tools for mimicing the API of the Common Data Model (CDM).

The CDM is a data model for representing a wide array of data. The
goal is to be a simple, universal interface to different datasets. This API is a Python
implementation in the spirit of the original Java interface in netCDF-Java.
"""

from collections import OrderedDict

import numpy as np

from ..deprecation import deprecated


[docs]@deprecated(0.8, alternative='XArray') class AttributeContainer(object): r"""Handle maintaining a list of netCDF attributes. Implements the attribute handling for other CDM classes. """ def __init__(self): r"""Initialize an :class:`AttributeContainer`.""" self._attrs = []
[docs] def ncattrs(self): r"""Get a list of the names of the netCDF attributes. Returns ------- List[str] """ return self._attrs
def __setattr__(self, key, value): """Handle setting attributes.""" if hasattr(self, '_attrs'): self._attrs.append(key) self.__dict__[key] = value def __delattr__(self, item): """Handle attribute deletion.""" self.__dict__.pop(item) if hasattr(self, '_attrs'): self._attrs.remove(item)
[docs]@deprecated(0.8, alternative='XArray') class Group(AttributeContainer): r"""Holds dimensions and variables. Every CDM dataset has at least a root group. """ def __init__(self, parent, name): r"""Initialize this :class:`Group`. Instead of constructing a :class:`Group` directly, you should use :meth:`~Group.createGroup`. Parameters ---------- parent : Group or None The parent Group for this one. Passing in :data:`None` implies that this is the root :class:`Group`. name : str The name of this group See Also -------- Group.createGroup """ self.parent = parent if parent: self.parent.groups[name] = self #: :desc: The name of the :class:`Group` #: :type: str self.name = name #: :desc: Any Groups nested within this one #: :type: dict[str, Group] self.groups = OrderedDict() #: :desc: Variables contained within this group #: :type: dict[str, Variable] self.variables = OrderedDict() #: :desc: Dimensions contained within this group #: :type: dict[str, Dimension] self.dimensions = OrderedDict() # Do this last so earlier attributes aren't captured super(Group, self).__init__() # CamelCase API names for netcdf4-python compatibility
[docs] def createGroup(self, name): # noqa: N802 """Create a new Group as a descendant of this one. Parameters ---------- name : str The name of the new Group. Returns ------- Group The newly created :class:`Group` """ grp = Group(self, name) self.groups[name] = grp return grp
[docs] def createDimension(self, name, size): # noqa: N802 """Create a new :class:`Dimension` in this :class:`Group`. Parameters ---------- name : str The name of the new Dimension. size : int The size of the Dimension Returns ------- Dimension The newly created :class:`Dimension` """ dim = Dimension(self, name, size) self.dimensions[name] = dim return dim
[docs] def createVariable(self, name, datatype, dimensions=(), fill_value=None, # noqa: N802 wrap_array=None): """Create a new Variable in this Group. Parameters ---------- name : str The name of the new Variable. datatype : str or numpy.dtype A valid Numpy dtype that describes the layout of the data within the Variable. dimensions : tuple[str], optional The dimensions of this Variable. Defaults to empty, which implies a scalar variable. fill_value : number, optional A scalar value that is used to fill the created storage. Defaults to None, which performs no filling, leaving the storage uninitialized. wrap_array : numpy.ndarray, optional Instead of creating an array, the Variable instance will assume ownership of the passed in array as its data storage. This is a performance optimization to avoid copying large data blocks. Defaults to None, which means a new array will be created. Returns ------- Variable The newly created :class:`Variable` """ var = Variable(self, name, datatype, dimensions, fill_value, wrap_array) self.variables[name] = var return var
def __str__(self): """Return a string representation of the Group.""" print_groups = [] if self.name: print_groups.append(self.name) if self.groups: print_groups.append('Groups:') for group in self.groups.values(): print_groups.append(str(group)) if self.dimensions: print_groups.append('\nDimensions:') for dim in self.dimensions.values(): print_groups.append(str(dim)) if self.variables: print_groups.append('\nVariables:') for var in self.variables.values(): print_groups.append(str(var)) if self.ncattrs(): print_groups.append('\nAttributes:') for att in self.ncattrs(): print_groups.append('\t{0}: {1}'.format(att, getattr(self, att))) return '\n'.join(print_groups)
[docs]@deprecated(0.8, alternative='XArray') class Dataset(Group): r"""Represents a set of data using the Common Data Model (CDM). This is currently only a wrapper around the root Group. """ def __init__(self): """Initialize a Dataset.""" super(Dataset, self).__init__(None, 'root')
[docs]@deprecated(0.8, alternative='XArray') class Variable(AttributeContainer): r"""Holds typed data (using a :class:`numpy.ndarray`), as well as attributes (e.g. units). In addition to its various attributes, the Variable supports getting *and* setting data using the ``[]`` operator and indices or slices. Getting data returns :class:`numpy.ndarray` instances. """ def __init__(self, group, name, datatype, dimensions, fill_value, wrap_array): """Initialize a Variable. Instead of constructing a Variable directly, you should use :meth:`Group.createVariable`. Parameters ---------- group : Group The parent :class:`Group` that owns this Variable. name : str The name of this Variable. datatype : str or numpy.dtype A valid Numpy dtype that describes the layout of each element of the data dimensions : tuple[str], optional The dimensions of this Variable. Defaults to empty, which implies a scalar variable. fill_value : scalar, optional A scalar value that is used to fill the created storage. Defaults to None, which performs no filling, leaving the storage uninitialized. wrap_array : numpy.ndarray, optional Instead of creating an array, the Variable instance will assume ownership of the passed in array as its data storage. This is a performance optimization to avoid copying large data blocks. Defaults to None, which means a new array will be created. See Also -------- Group.createVariable """ # Initialize internal vars self._group = group self._name = name self._dimensions = tuple(dimensions) # Set the storage--create/wrap as necessary shape = tuple(len(group.dimensions.get(d)) for d in dimensions) if wrap_array is not None: if shape != wrap_array.shape: raise ValueError('Array to wrap does not match dimensions.') self._data = wrap_array else: self._data = np.empty(shape, dtype=datatype) if fill_value is not None: self._data.fill(fill_value) # Do this last so earlier attributes aren't captured super(Variable, self).__init__() # Not a property to maintain compatibility with NetCDF4 python
[docs] def group(self): """Get the Group that owns this Variable. Returns ------- Group The parent Group. """ return self._group
@property def name(self): """str: the name of the variable.""" return self._name @property def size(self): """int: the total number of elements.""" return self._data.size @property def shape(self): """tuple[int]: Describes the size of the Variable along each of its dimensions.""" return self._data.shape @property def ndim(self): """int: the number of dimensions used by this variable.""" return self._data.ndim @property def dtype(self): """numpy.dtype: Describes the layout of each element of the data.""" return self._data.dtype @property def datatype(self): """numpy.dtype: Describes the layout of each element of the data.""" return self._data.dtype @property def dimensions(self): """tuple[str]: all the names of :class:`Dimension` used by this :class:`Variable`.""" return self._dimensions def __setitem__(self, ind, value): """Handle setting values on the Variable.""" self._data[ind] = value def __getitem__(self, ind): """Handle getting values from the Variable.""" return self._data[ind] def __str__(self): """Return a string representation of the Variable.""" groups = [str(type(self)) + ': {0.datatype} {0.name}({1})'.format(self, ', '.join(self.dimensions))] for att in self.ncattrs(): groups.append('\t{0}: {1}'.format(att, getattr(self, att))) if self.ndim: # Ensures we get the same string output on windows where shape contains longs shape = tuple(int(s) for s in self.shape) if self.ndim > 1: shape_str = str(shape) else: shape_str = str(shape[0]) groups.append('\tshape = ' + shape_str) return '\n'.join(groups)
# Punting on unlimited dimensions for now since we're relying upon numpy for storage # We don't intend to be a full file API or anything, just need to be able to represent # other files using a common API.
[docs]@deprecated(0.8, alternative='XArray') class Dimension(object): r"""Represent a shared dimension between different Variables. For instance, variables that are dependent upon a common set of times. """ def __init__(self, group, name, size=None): """Initialize a Dimension. Instead of constructing a Dimension directly, you should use ``Group.createDimension``. Parameters ---------- group : Group The parent Group that owns this Variable. name : str The name of this Variable. size : int or None, optional The size of the Dimension. Defaults to None, which implies an empty dimension. See Also -------- Group.createDimension """ self._group = group #: :desc: The name of the Dimension #: :type: str self.name = name #: :desc: The size of this Dimension #: :type: int self.size = size # Not a property to maintain compatibility with NetCDF4 python
[docs] def group(self): """Get the Group that owns this Dimension. Returns ------- Group The parent Group. """ return self._group
def __len__(self): """Return the length of this Dimension.""" return self.size def __str__(self): """Return a string representation of this Dimension.""" return '{0}: name = {1.name}, size = {1.size}'.format(type(self), self)
# Not sure if this lives long-term or not
[docs]@deprecated(0.8, alternative='XArray') def cf_to_proj(var): r"""Convert a Variable with projection information to a Proj.4 Projection instance. The attributes of this Variable must conform to the Climate and Forecasting (CF) netCDF conventions. Parameters ---------- var : Variable The projection variable with appropriate attributes. """ import pyproj kwargs = {'lat_0': var.latitude_of_projection_origin, 'a': var.earth_radius, 'b': var.earth_radius} if var.grid_mapping_name == 'lambert_conformal_conic': kwargs['proj'] = 'lcc' kwargs['lon_0'] = var.longitude_of_central_meridian kwargs['lat_1'] = var.standard_parallel kwargs['lat_2'] = var.standard_parallel elif var.grid_mapping_name == 'polar_stereographic': kwargs['proj'] = 'stere' kwargs['lon_0'] = var.straight_vertical_longitude_from_pole kwargs['lat_0'] = var.latitude_of_projection_origin kwargs['lat_ts'] = var.standard_parallel kwargs['x_0'] = False # Easting kwargs['y_0'] = False # Northing elif var.grid_mapping_name == 'mercator': kwargs['proj'] = 'merc' kwargs['lon_0'] = var.longitude_of_projection_origin kwargs['lat_ts'] = var.standard_parallel kwargs['x_0'] = False # Easting kwargs['y_0'] = False # Northing return pyproj.Proj(**kwargs)