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

Merge bounds handling into _DimensionalMetadata class. #7

Merged
merged 3 commits into from
Oct 23, 2019
Merged
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
153 changes: 82 additions & 71 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ def __init__(self, values, standard_name=None, long_name=None,
A dictionary containing other cf and user-defined attributes.

"""
# Note: this class includes bounds handling code for convenience, but
# this can only run within instances which are also Coords, because
# only they may actually have bounds. This parent class has no
# bounds-related getter/setter properties, and no bounds keywords in
# its __init__ or __copy__ methods. The only bounds-related behaviour
# it provides is a 'has_bounds()' method, which always returns False.

#: CF standard name of the quantity that the metadata represents.
self.standard_name = standard_name

Expand All @@ -111,6 +118,7 @@ def __init__(self, values, standard_name=None, long_name=None,
# Set up DataManager attributes and values.
self._values_dm = None
self._values = values
self._bounds_dm = None # Only ever set on Coord-derived instances.

def __getitem__(self, keys):
"""
Expand All @@ -124,6 +132,9 @@ def __getitem__(self, keys):
indexing.

"""
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.

# Fetch the values.
values = self._values_dm.core_data()

Expand All @@ -137,20 +148,15 @@ def __getitem__(self, keys):

# If the metadata is a coordinate and it has bounds, repeat the above
# with the bounds.
if isinstance(self, Coord):
if self.has_bounds():
bounds = self._bounds_dm.core_data()
_, bounds = iris.util._slice_data_with_keys(bounds, keys)
bounds = bounds.copy()
else:
bounds = None

copy_args = dict(points=values, bounds=bounds)
else:
copy_args = dict(values=values)
copy_args = {}
if self.has_bounds():
bounds = self._bounds_dm.core_data()
_, bounds = iris.util._slice_data_with_keys(bounds, keys)
# Pass into the copy method : for Coords, it has a 'bounds' key.
copy_args['bounds'] = bounds.copy()

# The new metadata is a copy of the old one with replaced content.
new_metadata = self.copy(**copy_args)
new_metadata = self.copy(values, **copy_args)

return new_metadata

Expand All @@ -166,6 +172,8 @@ def copy(self, values=None):
copied.

"""
# Note: this is overridden in Coord subclasses, to add bounds handling
# and a 'bounds' keyword.
new_metadata = copy.deepcopy(self)
if values is not None:
new_metadata._values_dm = None
Expand Down Expand Up @@ -262,6 +270,8 @@ def _str_dates(self, dates_as_numbers):
**kwargs)

def __str__(self):
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.
if self.units.is_time_reference():
fmt = '{cls}({values}{bounds}' \
', standard_name={self.standard_name!r}' \
Expand Down Expand Up @@ -289,6 +299,8 @@ def __str__(self):
return result

def __repr__(self):
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.
fmt = '{cls}({self._values!r}{bounds}' \
', standard_name={self.standard_name!r}, units={self.units!r}' \
'{other_metadata})'
Expand All @@ -302,6 +314,9 @@ def __repr__(self):
return result

def __eq__(self, other):
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.

eq = NotImplemented
# If the other object has a means of getting its definition, then do
# the comparison, otherwise return a NotImplemented to let Python try
Expand All @@ -310,9 +325,19 @@ def __eq__(self, other):
# metadata comparison
eq = self._as_defn() == other._as_defn()
# data values comparison
if eq:
if eq and eq is not NotImplemented:
eq = iris.util.array_equal(self._values, other._values,
withnans=True)

# Also consider bounds, if we have them.
# (N.B. though only Coords can ever actually *have* bounds).
if eq and eq is not NotImplemented:
if self.has_bounds() and other.has_bounds():
eq = iris.util.array_equal(self.bounds, other.bounds,
withnans=True)
else:
eq = not self.has_bounds() and not other.has_bounds()

return eq

def __ne__(self, other):
Expand Down Expand Up @@ -352,23 +377,47 @@ def __binary_operator__(self, other, mode_constant):
object would represent "10 kilometers".

"""
result = NotImplemented
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.

if (isinstance(other, _DimensionalMetadata) or
not isinstance(other, (int, float, np.number))):

def typename(obj):
if isinstance(obj, Coord):
result = 'Coord'
else:
# We don't really expect this, but do something anyway.
result = self.__class__.__name__
return result

emsg = '{selftype} {operator} {othertype}'.format(
selftype=typename(self),
operator=self._MODE_SYMBOL[mode_constant],
othertype=typename(other))
raise iris.exceptions.NotYetImplementedError(emsg)

if isinstance(other, (int, float, np.number)):
values = self._values_dm.core_data()
else:
# 'Other' is an array type : adjust points, and bounds if any.
result = NotImplemented

if mode_constant == self._MODE_ADD:
new_values = values + other
elif mode_constant == self._MODE_SUB:
new_values = values - other
elif mode_constant == self._MODE_MUL:
new_values = values * other
elif mode_constant == self._MODE_DIV:
new_values = values / other
elif mode_constant == self._MODE_RDIV:
new_values = other / values
def op(values):
if mode_constant == self._MODE_ADD:
new_values = values + other
elif mode_constant == self._MODE_SUB:
new_values = values - other
elif mode_constant == self._MODE_MUL:
new_values = values * other
elif mode_constant == self._MODE_DIV:
new_values = values / other
elif mode_constant == self._MODE_RDIV:
new_values = other / values
return new_values

new_values = op(self._values_dm.core_data())
result = self.copy(new_values)
if self.has_bounds():
result.bounds = op(self._bounds_dm.core_data())

return result

Expand Down Expand Up @@ -403,12 +452,18 @@ def __rmul__(self, other):
return self * other

def __neg__(self):
return self.copy(-self._core_values())
values = -self._core_values()
copy_args = {}
if self.has_bounds():
copy_args['bounds'] = -self.core_bounds()
return self.copy(values, **copy_args)

def convert_units(self, unit):
"""Change the units, converting the values of the metadata."""
# If the coord has units convert the values in points (and bounds if
# present).
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.
if self.units.is_unknown():
raise iris.exceptions.UnitConversionError(
'Cannot convert from unknown units. '
Expand Down Expand Up @@ -1396,17 +1451,6 @@ def _repr_other_metadata(self):
result += ', climatological={}'.format(self.climatological)
return result

def __eq__(self, other):
eq = super(Coord, self).__eq__(other=other)

if eq and eq is not NotImplemented:
if self.has_bounds() and other.has_bounds():
eq = iris.util.array_equal(self.bounds, other.bounds,
withnans=True)
else:
eq = self.bounds is None and other.bounds is None
return eq

def _as_defn(self):
defn = CoordDefn(self.standard_name, self.long_name, self.var_name,
self.units, self.attributes, self.coord_system,
Expand All @@ -1422,39 +1466,6 @@ def _as_defn(self):
def __hash__(self):
return hash(id(self))

def __binary_operator__(self, other, mode_constant):
if isinstance(other, Coord):
emsg = 'coord {} coord'.format(
self._MODE_SYMBOL[mode_constant])
raise iris.exceptions.NotYetImplementedError(emsg)

new_coord = super(Coord, self).__binary_operator__(
other=other, mode_constant=mode_constant)

if new_coord is not NotImplemented:
if self.has_bounds():
bounds = self._bounds_dm.core_data()

if mode_constant == self._MODE_ADD:
new_bounds = bounds + other
elif mode_constant == self._MODE_SUB:
new_bounds = bounds - other
elif mode_constant == self._MODE_MUL:
new_bounds = bounds * other
elif mode_constant == self._MODE_DIV:
new_bounds = bounds / other
elif mode_constant == self._MODE_RDIV:
new_bounds = other / bounds

else:
new_bounds = None
new_coord.bounds = new_bounds
return new_coord

def __neg__(self):
return self.copy(-self.core_points(),
-self.core_bounds() if self.has_bounds() else None)

def convert_units(self, unit):
"""
Change the coordinate's units, converting the values in its points
Expand Down