diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index c388c5fb7b..d0b03d9304 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -42,6 +42,11 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ and `@pp-mo`_ (reviewer) factored masking into the returned sum-of-weights calculation from :obj:`~iris.analysis.SUM`. (:pull:`4905`) +#. `@schlunma`_ fixed a bug which prevented using + :meth:`iris.cube.Cube.collapsed` on coordinates whose number of bounds + differs from 0 or 2. This enables the use of this method on mesh + coordinates. (:issue:`4672`, :pull:`4870`) + 💣 Incompatible Changes ======================= @@ -95,4 +100,4 @@ This document explains the changes made to Iris for this release .. _NEP13: https://numpy.org/neps/nep-0013-ufunc-overrides.html -.. _NEP18: https://numpy.org/neps/nep-0018-array-function-protocol.html \ No newline at end of file +.. _NEP18: https://numpy.org/neps/nep-0018-array-function-protocol.html diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 0a1aecb983..d0d471a634 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -2215,12 +2215,24 @@ def serialize(x): "Metadata may not be fully descriptive for {!r}." ) warnings.warn(msg.format(self.name())) - elif not self.is_contiguous(): - msg = ( - "Collapsing a non-contiguous coordinate. " - "Metadata may not be fully descriptive for {!r}." - ) - warnings.warn(msg.format(self.name())) + else: + try: + self._sanity_check_bounds() + except ValueError as exc: + msg = ( + "Cannot check if coordinate is contiguous: {} " + "Metadata may not be fully descriptive for {!r}. " + "Ignoring bounds." + ) + warnings.warn(msg.format(str(exc), self.name())) + self.bounds = None + else: + if not self.is_contiguous(): + msg = ( + "Collapsing a non-contiguous coordinate. " + "Metadata may not be fully descriptive for {!r}." + ) + warnings.warn(msg.format(self.name())) if self.has_bounds(): item = self.core_bounds() diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 08ed8d55e5..dca6ed3c1b 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -332,7 +332,8 @@ def test_dim_1d(self): ) for units in ["unknown", "no_unit", 1, "K"]: coord.units = units - collapsed_coord = coord.collapsed() + with self.assertNoWarningsRegexp(): + collapsed_coord = coord.collapsed() self.assertArrayEqual( collapsed_coord.points, np.mean(coord.points) ) @@ -474,6 +475,98 @@ def test_lazy_nd_points_and_bounds(self): self.assertArrayEqual(collapsed_coord.points, da.array([55])) self.assertArrayEqual(collapsed_coord.bounds, da.array([[-2, 112]])) + def test_numeric_nd_multidim_bounds_warning(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real, long_name="y") + + msg = ( + "Collapsing a multi-dimensional coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + with self.assertWarnsRegex(UserWarning, msg): + coord.collapsed() + + def test_lazy_nd_multidim_bounds_warning(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy, long_name="y") + + msg = ( + "Collapsing a multi-dimensional coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + with self.assertWarnsRegex(UserWarning, msg): + coord.collapsed() + + def test_numeric_nd_noncontiguous_bounds_warning(self): + self.setupTestArrays((3)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real, long_name="y") + + msg = ( + "Collapsing a non-contiguous coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + with self.assertWarnsRegex(UserWarning, msg): + coord.collapsed() + + def test_lazy_nd_noncontiguous_bounds_warning(self): + self.setupTestArrays((3)) + coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy, long_name="y") + + msg = ( + "Collapsing a non-contiguous coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + with self.assertWarnsRegex(UserWarning, msg): + coord.collapsed() + + def test_numeric_3_bounds(self): + + points = np.array([2.0, 6.0, 4.0]) + bounds = np.array([[1.0, 0.0, 3.0], [5.0, 4.0, 7.0], [3.0, 2.0, 5.0]]) + + coord = AuxCoord(points, bounds=bounds, long_name="x") + + msg = ( + r"Cannot check if coordinate is contiguous: Invalid operation for " + r"'x', with 3 bound\(s\). Contiguous bounds are only defined for " + r"1D coordinates with 2 bounds. Metadata may not be fully " + r"descriptive for 'x'. Ignoring bounds." + ) + with self.assertWarnsRegex(UserWarning, msg): + collapsed_coord = coord.collapsed() + + self.assertFalse(collapsed_coord.has_lazy_points()) + self.assertFalse(collapsed_coord.has_lazy_bounds()) + + self.assertArrayAlmostEqual(collapsed_coord.points, np.array([4.0])) + self.assertArrayAlmostEqual( + collapsed_coord.bounds, np.array([[2.0, 6.0]]) + ) + + def test_lazy_3_bounds(self): + + points = da.arange(3) * 2.0 + bounds = da.arange(3 * 3).reshape(3, 3) + + coord = AuxCoord(points, bounds=bounds, long_name="x") + + msg = ( + r"Cannot check if coordinate is contiguous: Invalid operation for " + r"'x', with 3 bound\(s\). Contiguous bounds are only defined for " + r"1D coordinates with 2 bounds. Metadata may not be fully " + r"descriptive for 'x'. Ignoring bounds." + ) + with self.assertWarnsRegex(UserWarning, msg): + collapsed_coord = coord.collapsed() + + self.assertTrue(collapsed_coord.has_lazy_points()) + self.assertTrue(collapsed_coord.has_lazy_bounds()) + + self.assertArrayAlmostEqual(collapsed_coord.points, da.array([2.0])) + self.assertArrayAlmostEqual( + collapsed_coord.bounds, da.array([[0.0, 4.0]]) + ) + class Test_is_compatible(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 944d216a30..f38d6ef35d 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -565,6 +565,67 @@ def test_no_lat_weighted_aggregator_mixed(self): self._assert_nowarn_collapse_without_weight(coords, warn) +class Test_collapsed_coord_with_3_bounds(tests.IrisTest): + def setUp(self): + self.cube = Cube([1, 2]) + + bounds = [[0.0, 1.0, 2.0], [2.0, 3.0, 4.0]] + lat = AuxCoord([1.0, 2.0], bounds=bounds, standard_name="latitude") + lon = AuxCoord([1.0, 2.0], bounds=bounds, standard_name="longitude") + + self.cube.add_aux_coord(lat, 0) + self.cube.add_aux_coord(lon, 0) + + def _assert_warn_cannot_check_contiguity(self, warn): + # Ensure that warning is raised. + for coord in ["latitude", "longitude"]: + msg = ( + f"Cannot check if coordinate is contiguous: Invalid " + f"operation for '{coord}', with 3 bound(s). Contiguous " + f"bounds are only defined for 1D coordinates with 2 " + f"bounds. Metadata may not be fully descriptive for " + f"'{coord}'. Ignoring bounds." + ) + self.assertIn(mock.call(msg), warn.call_args_list) + + def _assert_cube_as_expected(self, cube): + """Ensure that cube data and coordiantes are as expected.""" + self.assertArrayEqual(cube.data, np.array(3)) + + lat = cube.coord("latitude") + self.assertArrayAlmostEqual(lat.points, np.array([1.5])) + self.assertArrayAlmostEqual(lat.bounds, np.array([[1.0, 2.0]])) + + lon = cube.coord("longitude") + self.assertArrayAlmostEqual(lon.points, np.array([1.5])) + self.assertArrayAlmostEqual(lon.bounds, np.array([[1.0, 2.0]])) + + def test_collapsed_lat_with_3_bounds(self): + """Collapse latitude with 3 bounds.""" + with mock.patch("warnings.warn") as warn: + collapsed_cube = self.cube.collapsed("latitude", iris.analysis.SUM) + self._assert_warn_cannot_check_contiguity(warn) + self._assert_cube_as_expected(collapsed_cube) + + def test_collapsed_lon_with_3_bounds(self): + """Collapse longitude with 3 bounds.""" + with mock.patch("warnings.warn") as warn: + collapsed_cube = self.cube.collapsed( + "longitude", iris.analysis.SUM + ) + self._assert_warn_cannot_check_contiguity(warn) + self._assert_cube_as_expected(collapsed_cube) + + def test_collapsed_lat_lon_with_3_bounds(self): + """Collapse latitude and longitude with 3 bounds.""" + with mock.patch("warnings.warn") as warn: + collapsed_cube = self.cube.collapsed( + ["latitude", "longitude"], iris.analysis.SUM + ) + self._assert_warn_cannot_check_contiguity(warn) + self._assert_cube_as_expected(collapsed_cube) + + class Test_summary(tests.IrisTest): def setUp(self): self.cube = Cube(0)