Skip to content
Closed
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
108 changes: 28 additions & 80 deletions lib/iris/analysis/maths.py
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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):
Expand All @@ -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`.
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 3 additions & 12 deletions lib/iris/tests/test_basic_maths.py
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down
35 changes: 25 additions & 10 deletions lib/iris/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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()
24 changes: 15 additions & 9 deletions lib/iris/util.py
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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`.

Expand All @@ -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.')
Expand Down Expand Up @@ -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]
Expand Down