diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index af022da28f..b7831f1bd3 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office +# (C) British Crown Copyright 2010 - 2016, Met Office # # This file is part of Iris. # @@ -161,17 +161,11 @@ def _assert_matching_units(cube, other, operation_name): raise iris.exceptions.NotYetImplementedError(msg) -def add(cube, other, dim=None, ignore=True, in_place=False): +def add(cube, other, dim=None, in_place=False): """ Calculate the sum of two cubes, or the sum of a cube and a coordinate or scalar value. - When summing two cubes, they must both have the same coordinate - systems & data resolution. - - When adding a coordinate to a cube, they must both share the same - number of elements along a shared axis. - Args: * cube: @@ -192,22 +186,18 @@ def add(cube, other, dim=None, ignore=True, in_place=False): An instance of :class:`iris.cube.Cube`. """ + _assert_is_cube(cube) + _assert_matching_units(cube, other, 'add') op = operator.iadd if in_place else operator.add - return _add_subtract_common(op, 'add', cube, other, dim=dim, - ignore=ignore, in_place=in_place) + return _binary_op_common(op, 'add', cube, other, cube.units, dim=dim, + in_place=in_place) -def subtract(cube, other, dim=None, ignore=True, in_place=False): +def subtract(cube, other, dim=None, in_place=False): """ Calculate the difference between two cubes, or the difference between a cube and a coordinate or scalar value. - When subtracting two cubes, they must both have the same coordinate - systems & data resolution. - - When subtracting a coordinate to a cube, they must both share the - same number of elements along a shared axis. - Args: * cube: @@ -227,70 +217,12 @@ def subtract(cube, other, dim=None, ignore=True, in_place=False): Returns: An instance of :class:`iris.cube.Cube`. - """ - op = operator.isub if in_place else operator.sub - return _add_subtract_common(op, 'subtract', cube, other, - dim=dim, ignore=ignore, in_place=in_place) - - -def _add_subtract_common(operation_function, operation_name, cube, other, - dim=None, ignore=True, in_place=False): - """ - Function which shares common code between addition and subtraction - of cubes. - - operation_function - function which does the operation - (e.g. numpy.subtract) - operation_name - the public name of the operation (e.g. 'divide') - cube - the cube whose data is used as the first argument - to `operation_function` - other - the cube, coord, ndarray or number whose data is - used as the second argument - dim - dimension along which to apply `other` if it's a - coordinate that is not found in `cube` - ignore - The value of this argument is ignored. - .. deprecated:: 0.8 - in_place - whether or not to apply the operation in place to - `cube` and `cube.data` - """ _assert_is_cube(cube) - _assert_matching_units(cube, other, operation_name) - - 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) - - # provide a deprecation warning if the ignore keyword has been set - if ignore is not True: - warnings.warn('The "ignore" keyword has been deprecated in ' - 'add/subtract. This functionality is now automatic. ' - 'The provided value to "ignore" has been ignored, ' - 'and has been automatically calculated.') - - 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(operation_function, operation_name, cube, - other, cube.units, dim, 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 + _assert_matching_units(cube, other, 'subtract') + op = operator.isub if in_place else operator.sub + return _binary_op_common(op, 'subtract', cube, other, cube.units, dim=dim, + in_place=in_place) def multiply(cube, other, dim=None, in_place=False): @@ -310,6 +242,8 @@ def multiply(cube, other, dim=None, in_place=False): * dim: If supplying a coord with no match on the cube, you must supply the dimension to process. + * in_place: + Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. @@ -340,6 +274,8 @@ def divide(cube, other, dim=None, in_place=False): * dim: If supplying a coord with no match on the cube, you must supply the dimension to process. + * in_place: + Whether to create a new Cube, or alter the given "cube". Returns: An instance of :class:`iris.cube.Cube`. @@ -584,9 +520,11 @@ def _binary_op_common(operation_function, operation_name, cube, other, """ _assert_is_cube(cube) + coord_comp = None if isinstance(other, iris.coords.Coord): other = _broadcast_cube_coord_data(cube, other, operation_name, dim) elif isinstance(other, iris.cube.Cube): + coord_comp = iris.analysis.coord_comparison(cube, other) try: BA.broadcast_arrays(cube._my_data, other._my_data) except ValueError: @@ -609,7 +547,17 @@ def unary_func(x): (operation_function.__name__, type(x).__name__, type(other).__name__)) return ret - return _math_op_common(cube, unary_func, new_unit, in_place) + + new_cube = _math_op_common(cube, unary_func, new_unit, in_place) + + if coord_comp: + # Remove scalar coord mis-matches + mismatch = [coord_grp[0] for coord_grp in coord_comp['ignorable'] + if all(coord_grp)] + for coord in mismatch: + new_cube.remove_coord(coord) + + return new_cube def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 9fb94f8101..54454ca9a1 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3078,11 +3078,11 @@ def __hash__(self): return hash(id(self)) def __add__(self, other): - return iris.analysis.maths.add(self, other, ignore=True) + return iris.analysis.maths.add(self, other) __radd__ = __add__ def __sub__(self, other): - return iris.analysis.maths.subtract(self, other, ignore=True) + return iris.analysis.maths.subtract(self, other) __mul__ = iris.analysis.maths.multiply __rmul__ = iris.analysis.maths.multiply diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index ee18a91b53..d3246140d7 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office +# (C) British Crown Copyright 2010 - 2016, Met Office # # This file is part of Iris. # @@ -71,15 +71,6 @@ def test_minus(self): # Check that the subtraction has had no effect on the original self.assertCML(e, ('analysis', 'maths_original.cml')) - def test_minus_with_data_describing_coordinate(self): - a = self.cube - e = self.cube.copy() - lat = e.coord('latitude') - lat.points = lat.points+100 - - # Cannot ignore a axis describing coordinate - self.assertRaises(ValueError, iris.analysis.maths.subtract, a, e) - def test_minus_scalar(self): a = self.cube @@ -591,7 +582,7 @@ def vec_mag(u, v): c = a.copy() + 2 vec_mag_ufunc = np.frompyfunc(vec_mag, 2, 1) - my_ifunc = iris.analysis.maths.IFunc(vec_mag_ufunc, + my_ifunc = iris.analysis.maths.IFunc(vec_mag_ufunc, lambda x,y: (x + y).units) b = my_ifunc(a, c) @@ -613,7 +604,7 @@ def vec_mag_data_func(u_data, v_data): b = cs_ifunc(a, axis=1) ans = a.data.copy() - ans = np.cumsum(ans, axis=1) + ans = np.cumsum(ans, axis=1) self.assertArrayAlmostEqual(b.data, ans) diff --git a/lib/iris/tests/test_util.py b/lib/iris/tests/test_util.py index 5eb30e06f6..75b186b9e4 100644 --- a/lib/iris/tests/test_util.py +++ b/lib/iris/tests/test_util.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office +# (C) British Crown Copyright 2010 - 2016, Met Office # # This file is part of Iris. # @@ -80,7 +80,7 @@ def test_monotonic_strict(self): b = np.array([3, 5.3, 5.3]) self.assertNotMonotonic(b, strict=True) self.assertMonotonic(b, direction=1) - + b = b[::-1] self.assertNotMonotonic(b, strict=True) self.assertMonotonic(b, direction=-1) @@ -182,7 +182,7 @@ def test_trim_string_with_no_spaces(self): expected_length, 'Mismatch in expected length of clipped string. Length was %s, ' 'expected value is %s' % (len(result), expected_length)) - + class TestDescribeDiff(iris.tests.IrisTest): def test_identical(self): @@ -199,16 +199,16 @@ def test_different(self): # test incompatible attributes test_cube_a = stock.realistic_4d() test_cube_b = stock.realistic_4d() - + test_cube_a.attributes['Conventions'] = 'CF-1.5' test_cube_b.attributes['Conventions'] = 'CF-1.6' - + return_sio = six.StringIO() iris.util.describe_diff(test_cube_a, test_cube_b, output_file=return_sio) return_str = return_sio.getvalue() - + self.assertString(return_str, 'incompatible_attr.str.txt') - + # test incompatible names test_cube_a = stock.realistic_4d() test_cube_b = stock.realistic_4d() @@ -224,15 +224,15 @@ def test_different(self): # test incompatible unit test_cube_a = stock.realistic_4d() test_cube_b = stock.realistic_4d() - + test_cube_a.units = cf_units.Unit('m') return_sio = six.StringIO() iris.util.describe_diff(test_cube_a, test_cube_b, output_file=return_sio) return_str = return_sio.getvalue() - + self.assertString(return_str, 'incompatible_unit.str.txt') - + # test incompatible methods test_cube_a = stock.realistic_4d() test_cube_b = stock.realistic_4d().collapsed('model_level_number', iris.analysis.MEAN) @@ -334,6 +334,21 @@ def dim_to_aux(cube, coord_name): res = iris.util.as_compatible_shape(src, cube) self.assertEqual(res, expected) + def test_mismatched_dim(self): + cube = tests.stock.realistic_4d() + cube_wrong_coord = cube.copy() + cube_wrong_coord.coord('time').rename('spam') + self.assertRaises(ValueError, iris.util.as_compatible_shape, + cube_wrong_coord, cube) + + def test_match_auxcoord(self): + cube = tests.stock.realistic_4d() + cube_missing_dim_coord = cube.copy() + cube_missing_dim_coord.remove_coord('model_level_number') + # This dimension still has auxcoords so mapping should still work. + res = iris.util.as_compatible_shape(cube_missing_dim_coord, cube) + self.assertEqual(res, cube_missing_dim_coord) + if __name__ == '__main__': unittest.main() diff --git a/lib/iris/util.py b/lib/iris/util.py index ece3bff30e..da47505e81 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office +# (C) British Crown Copyright 2010 - 2016, Met Office # # This file is part of Iris. # @@ -1116,9 +1116,8 @@ def as_compatible_shape(src_cube, target_cube): This function can be used to add the dimensions that have been collapsed, aggregated or sliced out, promoting scalar coordinates to length one dimension coordinates where necessary. It operates by matching coordinate - metadata to infer the dimensions that need modifying, so the provided - cubes must have coordinates with the same metadata - (see :class:`iris.coords.CoordDefn`). + names to infer the dimensions that need modifying, so the provided + cubes must have coordinates with the same names along existing dimensions. .. note:: This function will load and copy the data payload of `src_cube`. @@ -1140,18 +1139,25 @@ def as_compatible_shape(src_cube, target_cube): for coord in target_cube.aux_coords + target_cube.dim_coords: dims = target_cube.coord_dims(coord) try: - collapsed_dims = src_cube.coord_dims(coord) + collapsed_dims = src_cube.coord_dims(coord.name()) except iris.exceptions.CoordinateNotFoundError: - continue + collapsed_dims = None if collapsed_dims: if len(collapsed_dims) == len(dims): for dim_from, dim_to in zip(dims, collapsed_dims): dim_mapping[dim_from] = dim_to elif dims: for dim_from in dims: - dim_mapping[dim_from] = None + if dim_from not in dim_mapping: + dim_mapping[dim_from] = None - if len(dim_mapping) != target_cube.ndim: + target_unmapped = len(dim_mapping) != target_cube.ndim + src_unmapped = False + for dim, length in enumerate(src_cube.shape): + if dim not in dim_mapping.values() and length > 1: + src_unmapped = True + + if target_unmapped or src_unmapped: raise ValueError('Insufficient or conflicting coordinate ' 'metadata. Cannot infer dimension mapping ' 'to restore cube dimensions.') @@ -1179,7 +1185,7 @@ def as_compatible_shape(src_cube, target_cube): def add_coord(coord): """Closure used to add a suitably reshaped coord to new_cube.""" - dims = target_cube.coord_dims(coord) + dims = target_cube.coord_dims(coord.name()) shape = [new_cube.shape[dim] for dim in dims] if not shape: shape = [1]