diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index 223432e076..75186741e4 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -318,11 +318,36 @@ def multiply(cube, other, dim=None, in_place=False): """ _assert_is_cube(cube) + op = operator.imul if in_place else operator.mul other_unit = getattr(other, 'units', '1') new_unit = cube.units * other_unit - op = operator.imul if in_place else operator.mul - return _binary_op_common(op, 'multiply', cube, other, new_unit, dim, - in_place=in_place) + + if isinstance(other, iris.cube.Cube): + # get a coordinate comparison of this cube and the cube to do the + # operation with + coord_comp = iris.analysis.coord_comparison(cube, other) + bad_coord_grps = (coord_comp['ungroupable_and_dimensioned'] + + coord_comp['resamplable']) + if bad_coord_grps: + raise ValueError('This operation cannot be performed as there are ' + 'differing coordinates (%s) remaining ' + 'which cannot be ignored.' + % ', '.join({coord_grp.name() for coord_grp + in bad_coord_grps})) + else: + coord_comp = None + + new_cube = _binary_op_common(op, 'multiply', cube, other, new_unit, dim, + in_place=in_place) + + if coord_comp: + # If a coordinate is to be ignored - remove it + ignore = filter(None, [coord_grp[0] for coord_grp + in coord_comp['ignorable']]) + for coord in ignore: + new_cube.remove_coord(coord) + + return new_cube def divide(cube, other, dim=None, in_place=False): @@ -348,14 +373,39 @@ def divide(cube, other, dim=None, in_place=False): """ _assert_is_cube(cube) - other_unit = getattr(other, 'units', '1') - new_unit = cube.units / other_unit try: op = operator.idiv if in_place else operator.div except AttributeError: op = operator.itruediv if in_place else operator.truediv - return _binary_op_common(op, 'divide', cube, other, new_unit, dim, - in_place=in_place) + other_unit = getattr(other, 'units', '1') + new_unit = cube.units / other_unit + + if isinstance(other, iris.cube.Cube): + # get a coordinate comparison of this cube and the cube to do the + # operation with + coord_comp = iris.analysis.coord_comparison(cube, other) + bad_coord_grps = (coord_comp['ungroupable_and_dimensioned'] + + coord_comp['resamplable']) + if bad_coord_grps: + raise ValueError('This operation cannot be performed as there are ' + 'differing coordinates (%s) remaining ' + 'which cannot be ignored.' + % ', '.join({coord_grp.name() for coord_grp + in bad_coord_grps})) + else: + coord_comp = None + + new_cube = _binary_op_common(op, 'divide', cube, other, new_unit, dim, + in_place=in_place) + + if coord_comp: + # If a coordinate is to be ignored - remove it + ignore = filter(None, [coord_grp[0] for coord_grp + in coord_comp['ignorable']]) + for coord in ignore: + new_cube.remove_coord(coord) + + return new_cube def exponentiate(cube, exponent, in_place=False): @@ -450,7 +500,7 @@ def log2(cube, in_place=False): * cube: An instance of :class:`iris.cube.Cube`. - Kwargs: + Kwargs:lib/iris/tests/unit/analysis/maths/test_subtract.py * in_place: Whether to create a new Cube, or alter the given "cube". diff --git a/lib/iris/tests/unit/analysis/maths/__init__.py b/lib/iris/tests/unit/analysis/maths/__init__.py index 7e5c53c41d..014c8e1817 100644 --- a/lib/iris/tests/unit/analysis/maths/__init__.py +++ b/lib/iris/tests/unit/analysis/maths/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -25,7 +25,9 @@ import numpy as np from iris.analysis import MEAN +from iris.coords import DimCoord from iris.cube import Cube +import iris.tests as tests import iris.tests.stock as stock @@ -158,3 +160,36 @@ def test_partial_mask_not_in_place(self): self.assertMaskedArrayEqual(com, res.data) self.assertIsNot(res, orig_cube) + + +class CubeArithmeticCoordsTest(tests.IrisTest): + # This class sets up pairs of cubes to test iris' ability to reject + # arithmetic operations on coordinates which do not match. + def SetUpNonMatching(self): + # On this cube pair, the coordinates to perform operations on do not + # match in either points array or name. + data = np.zeros((3, 4)) + a = DimCoord([1, 2, 3], long_name='a') + b = DimCoord([1, 2, 3, 4], long_name='b') + x = DimCoord([4, 5, 6], long_name='x') + y = DimCoord([5, 6, 7, 8], long_name='y') + + nomatch1 = Cube(data, dim_coords_and_dims=[(a, 0), (b, 1)]) + nomatch2 = Cube(data, dim_coords_and_dims=[(x, 0), (y, 1)]) + + return nomatch1, nomatch2 + + def SetUpReversed(self): + # On this cube pair, the coordinates to perform operations on have + # matching long names but the points array on one cube is reversed + # with respect to that on the other. + data = np.zeros((3, 4)) + a1 = DimCoord([1, 2, 3], long_name='a') + b1 = DimCoord([1, 2, 3, 4], long_name='b') + a2 = DimCoord([3, 2, 1], long_name='a') + b2 = DimCoord([1, 2, 3, 4], long_name='b') + + reversed1 = Cube(data, dim_coords_and_dims=[(a1, 0), (b1, 1)]) + reversed2 = Cube(data, dim_coords_and_dims=[(a2, 0), (b2, 1)]) + + return reversed1, reversed2 diff --git a/lib/iris/tests/unit/analysis/maths/test_add.py b/lib/iris/tests/unit/analysis/maths/test_add.py index 1e24c3cf18..8d678f8833 100644 --- a/lib/iris/tests/unit/analysis/maths/test_add.py +++ b/lib/iris/tests/unit/analysis/maths/test_add.py @@ -27,7 +27,8 @@ from iris.analysis.maths import add from iris.tests.unit.analysis.maths import \ - CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin + CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin, \ + CubeArithmeticCoordsTest @tests.skip_data @@ -54,5 +55,17 @@ def cube_func(self): return add +class TestCoordMatch(CubeArithmeticCoordsTest): + def test_no_match(self): + cube1, cube2 = self.SetUpNonMatching() + with self.assertRaises(ValueError): + add(cube1, cube2) + + def test_reversed_points(self): + cube1, cube2 = self.SetUpReversed() + with self.assertRaises(ValueError): + add(cube1, cube2) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/analysis/maths/test_divide.py b/lib/iris/tests/unit/analysis/maths/test_divide.py index 9b166787d8..f5d2c5b732 100644 --- a/lib/iris/tests/unit/analysis/maths/test_divide.py +++ b/lib/iris/tests/unit/analysis/maths/test_divide.py @@ -29,7 +29,8 @@ from iris.analysis.maths import divide from iris.cube import Cube from iris.tests.unit.analysis.maths import \ - CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin + CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin, \ + CubeArithmeticCoordsTest @tests.skip_data @@ -90,5 +91,17 @@ def test_masked_div_zero(self): self.assertMaskedArrayEqual(com, res, strict=True) +class TestCoordMatch(CubeArithmeticCoordsTest): + def test_no_match(self): + cube1, cube2 = self.SetUpNonMatching() + with self.assertRaises(ValueError): + divide(cube1, cube2) + + def test_reversed_points(self): + cube1, cube2 = self.SetUpReversed() + with self.assertRaises(ValueError): + divide(cube1, cube2) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/analysis/maths/test_multiply.py b/lib/iris/tests/unit/analysis/maths/test_multiply.py index 3bcc401ad1..5171fad78e 100644 --- a/lib/iris/tests/unit/analysis/maths/test_multiply.py +++ b/lib/iris/tests/unit/analysis/maths/test_multiply.py @@ -27,7 +27,8 @@ from iris.analysis.maths import multiply from iris.tests.unit.analysis.maths import \ - CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin + CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin, \ + CubeArithmeticCoordsTest @tests.skip_data @@ -54,5 +55,17 @@ def cube_func(self): return multiply +class TestCoordMatch(CubeArithmeticCoordsTest): + def test_no_match(self): + cube1, cube2 = self.SetUpNonMatching() + with self.assertRaises(ValueError): + multiply(cube1, cube2) + + def test_reversed_points(self): + cube1, cube2 = self.SetUpReversed() + with self.assertRaises(ValueError): + multiply(cube1, cube2) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/analysis/maths/test_subtract.py b/lib/iris/tests/unit/analysis/maths/test_subtract.py index 68e5f4de60..d982f04b98 100644 --- a/lib/iris/tests/unit/analysis/maths/test_subtract.py +++ b/lib/iris/tests/unit/analysis/maths/test_subtract.py @@ -27,7 +27,8 @@ from iris.analysis.maths import subtract from iris.tests.unit.analysis.maths import \ - CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin + CubeArithmeticBroadcastingTestMixin, CubeArithmeticMaskingTestMixin, \ + CubeArithmeticCoordsTest @tests.skip_data @@ -54,5 +55,16 @@ def cube_func(self): return subtract +class TestCoordMatch(CubeArithmeticCoordsTest): + def test_no_match(self): + cube1, cube2 = self.SetUpNonMatching() + with self.assertRaises(ValueError): + subtract(cube1, cube2) + + def test_reversed_points(self): + cube1, cube2 = self.SetUpReversed() + with self.assertRaises(ValueError): + subtract(cube1, cube2) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/analysis/stats/test_pearsonr.py b/lib/iris/tests/unit/analysis/stats/test_pearsonr.py index 41def1abc6..517ff59c94 100644 --- a/lib/iris/tests/unit/analysis/stats/test_pearsonr.py +++ b/lib/iris/tests/unit/analysis/stats/test_pearsonr.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -59,13 +59,15 @@ def test_incompatible_cubes(self): 'longitude') def test_compatible_cubes(self): - r = stats.pearsonr(self.cube_a, self.cube_b, ['latitude', 'longitude']) - self.assertArrayAlmostEqual(r.data, [0.81114936, - 0.81690538, - 0.79833135, - 0.81118674, - 0.79745386, - 0.81278484]) + with self.assertRaises(ValueError): + r = stats.pearsonr(self.cube_a, self.cube_b, + ['latitude', 'longitude']) + self.assertArrayAlmostEqual(r.data, [0.81114936, + 0.81690538, + 0.79833135, + 0.81118674, + 0.79745386, + 0.81278484]) def test_broadcast_cubes(self): r1 = stats.pearsonr(self.cube_a, self.cube_b[0, :, :], @@ -80,14 +82,16 @@ def test_broadcast_cubes(self): self.assertArrayEqual(r2.data, np.array(r_by_slice)) def test_compatible_cubes_weighted(self): - r = stats.pearsonr(self.cube_a, self.cube_b, ['latitude', 'longitude'], - self.weights) - self.assertArrayAlmostEqual(r.data, [0.79106045, - 0.79989169, - 0.78826918, - 0.79925855, - 0.79011544, - 0.80115837]) + with self.assertRaises(ValueError): + r = stats.pearsonr(self.cube_a, self.cube_b, + ['latitude', 'longitude'], + self.weights) + self.assertArrayAlmostEqual(r.data, [0.79106045, + 0.79989169, + 0.78826918, + 0.79925855, + 0.79011544, + 0.80115837]) def test_broadcast_cubes_weighted(self): r = stats.pearsonr(self.cube_a, self.cube_b[0, :, :],