Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coord points and bounds inheritance. #2553

Merged
merged 4 commits into from
May 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 149 additions & 150 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,13 +520,95 @@ def copy(self, points=None, bounds=None):

return new_coord

@abstractproperty
def points(self):
"""Property containing the points values as a numpy array"""
@classmethod
def from_coord(cls, coord):
"""Create a new Coord of this type, from the given coordinate."""
kwargs = {'points': coord.core_points(),
'bounds': coord.core_bounds(),
'standard_name': coord.standard_name,
'long_name': coord.long_name,
'var_name': coord.var_name,
'units': coord.units,
'attributes': coord.attributes,
'coord_system': copy.deepcopy(coord.coord_system)}
if issubclass(cls, DimCoord):
# DimCoord introduces an extra constructor keyword.
kwargs['circular'] = getattr(coord, 'circular', False)
return cls(**kwargs)

def _sanitise_array(self, src, ndmin):
if is_lazy_data(src):
# Lazy data : just ensure ndmin requirement.
ndims_missing = ndmin - src.ndim
if ndims_missing <= 0:
result = src
else:
extended_shape = tuple([1] * ndims_missing + list(src.shape))
result = src.reshape(extended_shape)
else:
# Real data : a few more things to do in this case.
# Ensure the array is writeable.
# NB. Returns the *same object* if src is already writeable.
result = np.require(src, requirements='W')
# Ensure the array has enough dimensions.
# NB. Returns the *same object* if result.ndim >= ndmin
result = np.array(result, ndmin=ndmin, copy=False)
# We don't need to copy the data, but we do need to have our
# own view so we can control the shape, etc.
result = result.view()
return result

def _points_getter(self):
"""The coordinate points values as a NumPy array."""
return self._points_dm.data

def _points_setter(self, points):
# Set the points to a new array - as long as it's the same shape.

# Ensure points has an ndmin of 1 and is either a numpy or lazy array.
# This will avoid Scalar coords with points of shape () rather
# than the desired (1,).
points = self._sanitise_array(points, 1)

# Set or update DataManager.
if self._points_dm is None:
self._points_dm = DataManager(points)
else:
self._points_dm.data = points

points = property(_points_getter, _points_setter)

def _bounds_getter(self):
"""
The coordinate bounds values, as a NumPy array,
or None if no bound values are defined.

.. note:: The shape of the bound array should be: ``points.shape +
(n_bounds, )``.

"""
bounds = None
if self.has_bounds():
bounds = self._bounds_dm.data
return bounds

def _bounds_setter(self, bounds):
# Ensure the bounds are a compatible shape.
if bounds is None:
self._bounds_dm = None
else:
bounds = self._sanitise_array(bounds, 2)
if self.shape != bounds.shape[:-1]:
raise ValueError("Bounds shape must be compatible with points "
"shape.")
if not self.has_bounds() \
or self.core_bounds().shape != bounds.shape:
# Construct a new bounds DataManager.
self._bounds_dm = DataManager(bounds)
else:
self._bounds_dm.data = bounds

@abstractproperty
def bounds(self):
"""Property containing the bound values as a numpy array"""
bounds = property(_bounds_getter, _bounds_setter)

def lazy_points(self):
"""
Expand Down Expand Up @@ -684,6 +766,15 @@ def _as_defn(self):
self.units, self.attributes, self.coord_system)
return defn

# Must supply __hash__ as Python 3 does not enable it if __eq__ is defined.
# NOTE: Violates "objects which compare equal must have the same hash".
# We ought to remove this, as equality of two coords can *change*, so they
# really should not be hashable.
# However, current code needs it, e.g. so we can put them in sets.
# Fixing it will require changing those uses. See #962 and #1772.
def __hash__(self):
return hash(id(self))

def __binary_operator__(self, other, mode_constant):
"""
Common code which is called by add, sub, mul and div
Expand Down Expand Up @@ -1429,16 +1520,6 @@ class DimCoord(Coord):
A coordinate that is 1D, numeric, and strictly monotonic.

"""
@staticmethod
def from_coord(coord):
"""Create a new DimCoord from the given coordinate."""
return DimCoord(coord.core_points(), standard_name=coord.standard_name,
long_name=coord.long_name, var_name=coord.var_name,
units=coord.units, bounds=coord.core_bounds(),
attributes=coord.attributes,
coord_system=copy.deepcopy(coord.coord_system),
circular=getattr(coord, 'circular', False))

@classmethod
def from_regular(cls, zeroth, step, count, standard_name=None,
long_name=None, var_name=None, units='1', attributes=None,
Expand Down Expand Up @@ -1536,10 +1617,13 @@ def __eq__(self, other):

# The __ne__ operator from Coord implements the not __eq__ method.

# This is necessary for merging, but probably shouldn't be used otherwise.
# See #962 and #1772.
def __hash__(self):
return hash(id(self))
# For Python 3, we must explicitly re-implement the '__hash__' method, as
# defining an '__eq__' has blocked its inheritance. See ...
# https://docs.python.org/3.1/reference/datamodel.html#object.__hash__
# "If a class that overrides __eq__() needs to retain the
# implementation of __hash__() from a parent class, the interpreter
# must be told this explicitly".
__hash__ = Coord.__hash__
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could use "super" here, if you think it would be preferable (it avoids saying who your parent is).
This way is more efficient though 😉


def __getitem__(self, key):
coord = super(DimCoord, self).__getitem__(key)
Expand Down Expand Up @@ -1580,29 +1664,29 @@ def _new_points_requirements(self, points):
if len(points) > 1 and not iris.util.monotonic(points, strict=True):
raise ValueError('The points array must be strictly monotonic.')

@property
def points(self):
"""The local points values as a read-only NumPy array."""
return self._points_dm.data

@points.setter
def points(self, points):
# We must realise points to check monotonicity.
def _points_setter(self, points):
# DimCoord always realises the points, to allow monotonicity checks.
copy = is_lazy_data(points)
points = as_concrete_data(points)
points = np.array(points, copy=copy, ndmin=1)
# Ensure it is an actual array, and also make our own distinct view
# so that we can make it read-only.
points = np.array(points, copy=copy)

# Set or update DataManager.
if self._points_dm is None:
self._points_dm = DataManager(points)
else:
self._points_dm.data = points
# Invoke the generic points setter.
super(DimCoord, self)._points_setter(points)

if self._points_dm is not None:
# Re-fetch the core array, as the super call may replace it.
points = self._points_dm.core_data()
# N.B. always a *real* array, as we realised 'points' at the start.

# Check validity requirements for dimension-coordinate points.
self._new_points_requirements(points)

# Checks for 1d, numeric, monotonic
self._new_points_requirements(self._points_dm.data)
# Make the array read-only.
points.flags.writeable = False

# Make the array read-only.
self._points_dm.data.flags.writeable = False
points = property(Coord._points_getter, _points_setter)

def _new_bounds_requirements(self, bounds):
"""
Expand Down Expand Up @@ -1639,37 +1723,28 @@ def _new_bounds_requirements(self, bounds):
raise ValueError('The direction of monotonicity must be '
'consistent across all bounds')

@property
def bounds(self):
"""
The bounds values as a read-only NumPy array, or None if no
bounds have been set.

"""
bounds = None
if self.has_bounds():
bounds = self._bounds_dm.data
return bounds

@bounds.setter
def bounds(self, bounds):
if bounds is None:
self._bounds_dm = None
else:
def _bounds_setter(self, bounds):
if bounds is not None:
# Ensure we have a realised array of new bounds values.
copy = is_lazy_data(bounds)
bounds = as_concrete_data(bounds)
bounds = np.array(bounds, copy=copy, ndmin=2)
# Checks for appropriate values for the new bounds.
bounds = np.array(bounds, copy=copy)

# Invoke the generic bounds setter.
super(DimCoord, self)._bounds_setter(bounds)

if self._bounds_dm is not None:
# Re-fetch the core array, as the super call may replace it.
bounds = self._bounds_dm.core_data()
# N.B. always a *real* array, as we realised 'bounds' at the start.

# Check validity requirements for dimension-coordinate bounds.
self._new_bounds_requirements(bounds)

# Ensure the array is read-only.
bounds.flags.writeable = False
if not self.has_bounds() or self.bounds.shape != bounds.shape:
# Construct a new bounds DataManager.
self._bounds_dm = DataManager(bounds)
else:
self._bounds_dm.data = bounds

bounds = property(Coord._bounds_getter, _bounds_setter)

def is_monotonic(self):
return True
Expand All @@ -1683,97 +1758,21 @@ def xml_element(self, doc):


class AuxCoord(Coord):
"""A CF auxiliary coordinate."""
@staticmethod
def from_coord(coord):
"""Create a new AuxCoord from the given coordinate."""
new_coord = AuxCoord(coord.points, standard_name=coord.standard_name,
long_name=coord.long_name,
var_name=coord.var_name,
units=coord.units, bounds=coord.bounds,
attributes=coord.attributes,
coord_system=copy.deepcopy(coord.coord_system))

return new_coord

def _sanitise_array(self, src, ndmin):
if is_lazy_data(src):
# Lazy data : just ensure ndmin requirement.
ndims_missing = ndmin - src.ndim
if ndims_missing <= 0:
result = src
else:
extended_shape = tuple([1] * ndims_missing + list(src.shape))
result = src.reshape(extended_shape)
else:
# Real data : a few more things to do in this case.
# Ensure the array is writeable.
# NB. Returns the *same object* if src is already writeable.
result = np.require(src, requirements='W')
# Ensure the array has enough dimensions.
# NB. Returns the *same object* if result.ndim >= ndmin
result = np.array(result, ndmin=ndmin, copy=False)
# We don't need to copy the data, but we do need to have our
# own view so we can control the shape, etc.
result = result.view()
return result

@property
def points(self):
"""The local points values as a read-only NumPy array."""
return self._points_dm.data

@points.setter
def points(self, points):
# Set the points to a new array - as long as it's the same shape.

# Ensure points has an ndmin of 1 and is either a numpy or lazy array.
# This will avoid Scalar coords with points of shape () rather
# than the desired (1,).
points = self._sanitise_array(points, 1)

# Set or update DataManager.
if self._points_dm is None:
self._points_dm = DataManager(points)
else:
self._points_dm.data = points
"""
A CF auxiliary coordinate.

@property
def bounds(self):
"""
Property containing the bound values, as a NumPy array,
or None if no bound values are defined.
.. note::

.. note:: The shape of the bound array should be: ``points.shape +
(n_bounds, )``.
There are currently no specific properties of :class:`AuxCoord`,
everything is inherited from :class:`Coord`.

"""
bounds = None
if self.has_bounds():
bounds = self._bounds_dm.data
return bounds

@bounds.setter
def bounds(self, bounds):
# Ensure the bounds are a compatible shape.
if bounds is None:
self._bounds_dm = None
else:
bounds = self._sanitise_array(bounds, 2)
if self.shape != bounds.shape[:-1]:
raise ValueError("Bounds shape must be compatible with points "
"shape.")
if not self.has_bounds() \
or self.core_bounds().shape != bounds.shape:
# Construct a new bounds DataManager.
self._bounds_dm = DataManager(bounds)
else:
self._bounds_dm.data = bounds

# This is necessary for merging, but probably shouldn't be used otherwise.
# See #962 and #1772.
def __hash__(self):
return hash(id(self))
"""
# Logically, :class:`Coord` is an abstract class and all actual coords must
# be members of some concrete subclass, i.e. an :class:`AuxCoord` or
# a :class:`DimCoord`.
# So we retain :class:`AuxCoord` as a distinct concrete subclass.
# This provides clarity, backwards compatibility, and so we can add
# AuxCoord-specific code if needed in future.


class CellMeasure(six.with_metaclass(ABCMeta, CFVariableMixin)):
Expand Down
9 changes: 6 additions & 3 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -3081,9 +3081,12 @@ def __ne__(self, other):
result = not result
return result

# Must supply __hash__, Python 3 does not enable it if __eq__ is defined
# This is necessary for merging, but probably shouldn't be used otherwise.
# See #962 and #1772.
# Must supply __hash__ as Python 3 does not enable it if __eq__ is defined.
# NOTE: Violates "objects which compare equal must have the same hash".
# We ought to remove this, as equality of two cube can *change*, so they
# really should not be hashable.
# However, current code needs it, e.g. so we can put them in sets.
# Fixing it will require changing those uses. See #962 and #1772.
def __hash__(self):
return hash(id(self))

Expand Down
5 changes: 3 additions & 2 deletions lib/iris/tests/test_coord_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,11 @@ def test_dim_coord_restrictions(self):
'monotonicity.*consistent.*all bounds'):
iris.coords.DimCoord([1, 2, 3], bounds=[[1, 12], [2, 9], [3, 6]])
# shapes of points and bounds
with self.assertRaisesRegexp(ValueError, 'shape of the bounds array'):
msg = 'Bounds shape must be compatible with points shape'
with self.assertRaisesRegexp(ValueError, msg):
iris.coords.DimCoord([1, 2, 3], bounds=[0.5, 1.5, 2.5, 3.5])
# another example of shapes of points and bounds
with self.assertRaisesRegexp(ValueError, 'shape of the bounds array'):
with self.assertRaisesRegexp(ValueError, msg):
iris.coords.DimCoord([1, 2, 3], bounds=[[0.5, 1.5], [1.5, 2.5]])

# numeric
Expand Down
Loading