# 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)