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

Fb fix cube coord arithmetic #4159

Merged
merged 3 commits into from
May 27, 2021
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
8 changes: 7 additions & 1 deletion docs/iris/src/whatsnew/3.0.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ This document explains the changes made to Iris for this release
where one cell's bounds align with the requested maximum and minimum, as
reported in :issue:`3391`. (:pull:`4059`)

#. `@bjlittle`_ resolved a regression in arithmetic behaviour between a coordinate
and a cube which resulted in a ``NotYetImplementedError`` being raised, as reported
in :issue:`4000`. This fix supports ``+``, ``-``, ``*``, and ``/`` operations
between a coordinate and a cube, and for convenience additionally includes
:meth:`iris.cube.Cube.__neg__` support. (:pull:`4159`)

📚 **Documentation**

#. `@bjlittle`_ updated the ``intersphinx_mapping`` and fixed documentation
Expand All @@ -46,7 +52,7 @@ This document explains the changes made to Iris for this release
the dask 'test access'. This makes loading of netcdf files with a large number of variables significantly faster.
(:pull:`4135`)

Note that, the contributions labelled ``pre-v3.1.0`` are part of the forthcoming
Note that, the above contributions labelled with ``pre-v3.1.0`` are part of the forthcoming
Iris v3.1.0 release, but require to be included in this patch release.


Expand Down
37 changes: 14 additions & 23 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,28 +405,15 @@ def __binary_operator__(self, other, mode_constant):
# 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),
if isinstance(other, _DimensionalMetadata):
emsg = (
f"{self.__class__.__name__} "
f"{self._MODE_SYMBOL[mode_constant]} "
f"{other.__class__.__name__}"
)
raise iris.exceptions.NotYetImplementedError(emsg)

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

def op(values):
if mode_constant == self._MODE_ADD:
Expand All @@ -443,8 +430,14 @@ def op(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())
else:
# must return NotImplemented to ensure invocation of any
# associated reflected operator on the "other" operand
# see https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types
result = NotImplemented

return result

Expand All @@ -463,8 +456,7 @@ def __div__(self, other):
def __truediv__(self, other):
return self.__binary_operator__(other, self._MODE_DIV)

def __radd__(self, other):
return self + other
__radd__ = __add__

def __rsub__(self, other):
return (-self) + other
Expand All @@ -475,8 +467,7 @@ def __rdiv__(self, other):
def __rtruediv__(self, other):
return self.__binary_operator__(other, self._MODE_RDIV)

def __rmul__(self, other):
return self * other
__rmul__ = __mul__

def __neg__(self):
values = -self._core_values()
Expand Down
28 changes: 20 additions & 8 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -3754,37 +3754,49 @@ def __ne__(self, other):
def __hash__(self):
return hash(id(self))

def __add__(self, other):
return iris.analysis.maths.add(self, other)
__add__ = iris.analysis.maths.add

def __iadd__(self, other):
return iris.analysis.maths.add(self, other, in_place=True)

__radd__ = __add__

def __sub__(self, other):
return iris.analysis.maths.subtract(self, other)
__sub__ = iris.analysis.maths.subtract

def __isub__(self, other):
return iris.analysis.maths.subtract(self, other, in_place=True)

def __rsub__(self, other):
return (-self) + other

__mul__ = iris.analysis.maths.multiply
__rmul__ = iris.analysis.maths.multiply

def __imul__(self, other):
return iris.analysis.maths.multiply(self, other, in_place=True)

__rmul__ = __mul__

__div__ = iris.analysis.maths.divide

def __idiv__(self, other):
return iris.analysis.maths.divide(self, other, in_place=True)

__truediv__ = iris.analysis.maths.divide
def __rdiv__(self, other):
data = 1 / self.core_data()
reciprocal = self.copy(data=data)
return iris.analysis.maths.multiply(reciprocal, other)

def __itruediv__(self, other):
return iris.analysis.maths.divide(self, other, in_place=True)
__truediv__ = __div__

__itruediv__ = __idiv__

__rtruediv__ = __rdiv__

__pow__ = iris.analysis.maths.exponentiate

def __neg__(self):
return self.copy(data=-self.core_data())

# END OPERATOR OVERLOADS

def collapsed(self, coords, aggregator, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion lib/iris/fileformats/netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2655,7 +2655,7 @@ def save(
local_keys.update(different_value_keys)

def is_valid_packspec(p):
""" Only checks that the datatype is valid. """
"""Only checks that the datatype is valid."""
if isinstance(p, dict):
if "dtype" in p:
return is_valid_packspec(p["dtype"])
Expand Down
2 changes: 1 addition & 1 deletion lib/iris/tests/integration/test_netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ def test_single_packed_signed(self):
self._single_test("i2", "single_packed_signed.cdl")

def test_single_packed_unsigned(self):
"""Test saving a single CF-netCDF file with packing into unsigned. """
"""Test saving a single CF-netCDF file with packing into unsigned."""
self._single_test("u1", "single_packed_unsigned.cdl")

def test_single_packed_manual_scale(self):
Expand Down
107 changes: 88 additions & 19 deletions lib/iris/tests/test_basic_maths.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

import iris
import iris.analysis.maths
import iris.coords
import iris.exceptions
from iris.coords import AuxCoord, DimCoord
from iris.cube import Cube
from iris.exceptions import NotYetImplementedError
import iris.tests.stock


Expand Down Expand Up @@ -112,12 +113,12 @@ def test_minus_coord(self):

xdim = a.ndim - 1
ydim = a.ndim - 2
c_x = iris.coords.DimCoord(
c_x = DimCoord(
points=np.arange(a.shape[xdim]),
long_name="x_coord",
units=self.cube.units,
)
c_y = iris.coords.AuxCoord(
c_y = AuxCoord(
points=np.arange(a.shape[ydim]),
long_name="y_coord",
units=self.cube.units,
Expand Down Expand Up @@ -150,12 +151,12 @@ def test_addition_coord(self):

xdim = a.ndim - 1
ydim = a.ndim - 2
c_x = iris.coords.DimCoord(
c_x = DimCoord(
points=np.arange(a.shape[xdim]),
long_name="x_coord",
units=self.cube.units,
)
c_y = iris.coords.AuxCoord(
c_y = AuxCoord(
points=np.arange(a.shape[ydim]),
long_name="y_coord",
units=self.cube.units,
Expand Down Expand Up @@ -195,20 +196,20 @@ def test_addition_fail(self):

xdim = a.ndim - 1
ydim = a.ndim - 2
c_axis_length_fail = iris.coords.DimCoord(
c_axis_length_fail = DimCoord(
points=np.arange(a.shape[ydim]),
long_name="x_coord",
units=self.cube.units,
)
c_unit_fail = iris.coords.AuxCoord(
c_unit_fail = AuxCoord(
points=np.arange(a.shape[xdim]), long_name="x_coord", units="volts"
)

self.assertRaises(
ValueError, iris.analysis.maths.add, a, c_axis_length_fail
)
self.assertRaises(
iris.exceptions.NotYetImplementedError,
NotYetImplementedError,
iris.analysis.maths.add,
a,
c_unit_fail,
Expand Down Expand Up @@ -464,7 +465,7 @@ def test_divide_by_coordinate_dim2(self):
def test_divide_by_singular_coordinate(self):
a = self.cube

coord = iris.coords.DimCoord(points=2, long_name="foo", units="1")
coord = DimCoord(points=2, long_name="foo", units="1")
c = iris.analysis.maths.divide(a, coord)
self.assertCML(c, ("analysis", "division_by_singular_coord.cml"))

Expand All @@ -474,7 +475,7 @@ def test_divide_by_singular_coordinate(self):
def test_divide_by_different_len_coord(self):
a = self.cube

coord = iris.coords.DimCoord(
coord = DimCoord(
points=np.arange(10) * 2 + 5,
standard_name="longitude",
units="degrees",
Expand Down Expand Up @@ -686,12 +687,12 @@ def setUp(self):
self.data_1u = np.array([[9, 9, 9], [8, 8, 8]], dtype=np.uint64)
self.data_2u = np.array([[3, 3, 3], [2, 2, 2]], dtype=np.uint64)

self.cube_1f = iris.cube.Cube(self.data_1f)
self.cube_2f = iris.cube.Cube(self.data_2f)
self.cube_1i = iris.cube.Cube(self.data_1i)
self.cube_2i = iris.cube.Cube(self.data_2i)
self.cube_1u = iris.cube.Cube(self.data_1u)
self.cube_2u = iris.cube.Cube(self.data_2u)
self.cube_1f = Cube(self.data_1f)
self.cube_2f = Cube(self.data_2f)
self.cube_1i = Cube(self.data_1i)
self.cube_2i = Cube(self.data_2i)
self.cube_1u = Cube(self.data_1u)
self.cube_2u = Cube(self.data_2u)

self.ops = (operator.add, operator.sub, operator.mul, operator.truediv)
self.iops = (
Expand All @@ -701,6 +702,22 @@ def setUp(self):
operator.itruediv,
)

def common_neg(self, cube, data):
result1 = -cube
result2 = -data
self.assertIsInstance(result1, Cube)
self.assertIsNot(result1, cube)
self.assertArrayAlmostEqual(result1.data, result2)

def test_neg_f(self):
self.common_neg(self.cube_1f, self.data_1f)

def test_neg_i(self):
self.common_neg(self.cube_1i, self.data_1i)

def test_neg_u(self):
self.common_neg(self.cube_1u, self.data_1u)

def test_operator(self):
for test_op in self.ops:
result1 = test_op(self.cube_1f, self.cube_2f)
Expand Down Expand Up @@ -849,7 +866,7 @@ def setUp(self):
mask=[[0, 1, 0], [0, 0, 1]],
dtype=np.float64,
)
self.cube = iris.cube.Cube(self.data)
self.cube = Cube(self.data)

def test_incompatible_dimensions(self):
data3 = ma.MaskedArray(
Expand All @@ -863,9 +880,61 @@ def test_increase_cube_dimensionality(self):
with self.assertRaises(ValueError):
# This would increase the dimensionality of the cube
# due to auto-broadcasting.
cube_x = iris.cube.Cube(ma.MaskedArray([[9]], mask=[[0]]))
cube_x = Cube(ma.MaskedArray([[9]], mask=[[0]]))
cube_x + ma.MaskedArray([[3, 3, 3, 3]], mask=[[0, 1, 0, 1]])


class TestCoordMathOperations(tests.IrisTest):
def setUp(self):
self.value_cube = 100
self.value = 10
self.cube = Cube([self.value_cube])
self.dim = DimCoord([self.value])
self.aux = AuxCoord([self.value])
self.i = int(self.value)
self.f = float(self.value)
self.np_i = np.int32(self.value)
self.np_f = np.float32(self.value)
self.numbers = (self.i, self.f, self.np_i, self.np_f)
self.ops = (operator.add, operator.sub, operator.mul, operator.truediv)
self.symbols = ("+", "-", "*", "/")

def test_coord_op_coord__fail(self):
for op, symbol in zip(self.ops, self.symbols):
emsg = f"AuxCoord \{symbol} DimCoord" # noqa: W605
with self.assertRaisesRegex(NotYetImplementedError, emsg):
_ = op(self.aux, self.dim)

def test_coord_op_number(self):
for op in self.ops:
for number in self.numbers:
actual = op(self.aux, number)
expected = op(self.value, number)
self.assertIsInstance(actual, AuxCoord)
self.assertEqual(expected, actual.points)

def test_number_op_coord(self):
for op in self.ops:
for number in self.numbers:
actual = op(number, self.dim)
expected = op(number, self.value)
self.assertIsInstance(actual, DimCoord)
self.assertEqual(expected, actual.points)

def test_coord_op_cube(self):
for op in self.ops:
actual = op(self.aux, self.cube)
expected = op(self.value, self.value_cube)
self.assertIsInstance(actual, Cube)
self.assertEqual(expected, actual.data)

def test_cube_op_coord(self):
for op in self.ops:
actual = op(self.cube, self.dim)
expected = op(self.value_cube, self.value)
self.assertIsInstance(actual, Cube)
self.assertEqual(actual.data, expected)


if __name__ == "__main__":
tests.main()
2 changes: 1 addition & 1 deletion lib/iris/tests/test_cube_to_pp.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def test_365_calendar_export(self):


class FakePPEnvironment:
""" fake a minimal PP environment for use in cross-section coords, as in PP save rules """
"""fake a minimal PP environment for use in cross-section coords, as in PP save rules"""

y = [1, 2, 3, 4]
z = [111, 222, 333, 444]
Expand Down
Loading