diff --git a/docs/iris/src/whatsnew/contributions_2.3.0/bugfix_2019-Apr-03_partial_collapse_bounded_coords.txt b/docs/iris/src/whatsnew/contributions_2.3.0/bugfix_2019-Apr-03_partial_collapse_bounded_coords.txt new file mode 100644 index 0000000000..27f1aad5c6 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.3.0/bugfix_2019-Apr-03_partial_collapse_bounded_coords.txt @@ -0,0 +1,2 @@ +* :meth:`iris.cube.Cube.collapsed` now handles partial collapsing of +multidimensional coordinates that have bounds. diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 6487b3f7e6..eee0a75643 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -1356,16 +1356,22 @@ def serialize(x): 'Metadata may not be fully descriptive for {!r}.' warnings.warn(msg.format(self.name())) - # Determine the array library for stacking - al = da if self.has_bounds() \ - and _lazy.is_lazy_data(self.core_bounds()) else np + if self.has_bounds(): + item = self.core_bounds() + if dims_to_collapse is not None: + # Express main dims_to_collapse as non-negative integers + # and add the last (bounds specific) dimension. + dims_to_collapse = tuple( + dim % self.ndim for dim in dims_to_collapse) + (-1,) + else: + item = self.core_points() - item = al.concatenate(self.core_bounds()) if self.has_bounds() \ - else self.core_points() + # Determine the array library for stacking + al = da if _lazy.is_lazy_data(item) else np # Calculate the bounds and points along the right dims bounds = al.stack([item.min(axis=dims_to_collapse), - item.max(axis=dims_to_collapse)]).T + item.max(axis=dims_to_collapse)], axis=-1) points = al.array(bounds.sum(axis=-1) * 0.5, dtype=self.dtype) # Create the new collapsed coordinate. diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index d42448d609..0ea7b133ce 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2018, Met Office +# (C) British Crown Copyright 2013 - 2019, Met Office # # This file is part of Iris. # @@ -28,6 +28,7 @@ import warnings import numpy as np +import dask.array as da import iris from iris.coords import DimCoord, AuxCoord, Coord @@ -303,9 +304,46 @@ def test_numeric_nd(self): [4, 10], [5, 11]])) - def test_lazy_nd_bounds(self): - import dask.array as da + def test_numeric_nd_bounds_all(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real) + + collapsed_coord = coord.collapsed() + self.assertArrayEqual(collapsed_coord.points, np.array([55])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 112]])) + + def test_numeric_nd_bounds_second(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real) + collapsed_coord = coord.collapsed(1) + self.assertArrayEqual(collapsed_coord.points, np.array([15, 55, 95])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 32], + [38, 72], + [78, 112]])) + def test_numeric_nd_bounds_first(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real) + # ... and the other.. + collapsed_coord = coord.collapsed(0) + self.assertArrayEqual( + collapsed_coord.points, np.array([40, 50, 60, 70])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 82], + [8, 92], + [18, 102], + [28, 112]])) + + def test_numeric_nd_bounds_last(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real) + # ... and again with -ve dimension specification. + collapsed_coord = coord.collapsed(-1) + self.assertArrayEqual(collapsed_coord.points, np.array([15, 55, 95])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 32], + [38, 72], + [78, 112]])) + + def test_lazy_nd_bounds_all(self): self.setupTestArrays((3, 4)) coord = AuxCoord(self.pts_real, bounds=self.bds_lazy) @@ -319,8 +357,39 @@ def test_lazy_nd_bounds(self): self.assertArrayEqual(collapsed_coord.points, np.array([55])) self.assertArrayEqual(collapsed_coord.bounds, da.array([[-2, 112]])) + def test_lazy_nd_bounds_second(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_lazy) + + collapsed_coord = coord.collapsed(1) + self.assertArrayEqual(collapsed_coord.points, np.array([15, 55, 95])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 32], + [38, 72], + [78, 112]])) + + def test_lazy_nd_bounds_first(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_lazy) + + collapsed_coord = coord.collapsed(0) + self.assertArrayEqual( + collapsed_coord.points, np.array([40, 50, 60, 70])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 82], + [8, 92], + [18, 102], + [28, 112]])) + + def test_lazy_nd_bounds_last(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_lazy) + + collapsed_coord = coord.collapsed(-1) + self.assertArrayEqual(collapsed_coord.points, np.array([15, 55, 95])) + self.assertArrayEqual(collapsed_coord.bounds, np.array([[-2, 32], + [38, 72], + [78, 112]])) + def test_lazy_nd_points_and_bounds(self): - import dask.array as da self.setupTestArrays((3, 4)) coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy)