From 30b5e0af67b27d2f927c116980dfc4a0ba9e3ac2 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 20 Jul 2022 14:30:44 +0200 Subject: [PATCH 1/6] Allowed collapsing over coordinates with nbounds!=0,2 --- lib/iris/coords.py | 24 ++++++-- lib/iris/tests/unit/coords/test_Coord.py | 70 ++++++++++++++++++++++++ lib/iris/tests/unit/cube/test_Cube.py | 61 +++++++++++++++++++++ 3 files changed, 149 insertions(+), 6 deletions(-) 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..aac18751e4 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -474,6 +474,76 @@ 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_3_bounds_warning(self): + points = np.array([2.0]) + bounds = np.array([[1.0, 0.0, 3.0]]) + + coord = AuxCoord(points, bounds=bounds, long_name="x") + + msg = ( + r"Cannot check if coordinate is contiguous: Invalid operation " + r"for 'x', with 3 bound\(s\). Contiguous bounds are only " + r"defined for 1D coordinates with 2 bounds. Metadata may not " + r"be fully descriptive for 'x'. Ignoring bounds." + ) + with warnings.catch_warnings(): + # Cause all warnings to raise Exceptions + warnings.simplefilter("error") + with self.assertRaisesRegex(UserWarning, msg): + coord.collapsed() + + def test_lazy_3_bounds_warning(self): + points = da.arange(1) + bounds = da.arange(1 * 3).reshape(1, 3) + + coord = AuxCoord(points, bounds=bounds, long_name="y") + + msg = ( + r"Cannot check if coordinate is contiguous: Invalid operation " + r"for 'y', with 3 bound\(s\). Contiguous bounds are only " + r"defined for 1D coordinates with 2 bounds. Metadata may not " + r"be fully descriptive for 'y'. Ignoring bounds." + ) + with warnings.catch_warnings(): + # Cause all warnings to raise Exceptions + warnings.simplefilter("error") + with self.assertRaisesRegex(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) + + 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) + + 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) From 445aab5fa96c2f95bdc01ed66f0803336b3d0d48 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 20 Jul 2022 14:43:07 +0200 Subject: [PATCH 2/6] Simplified test --- lib/iris/tests/unit/coords/test_Coord.py | 62 +++++++++--------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index aac18751e4..d8ee57ea63 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -474,50 +474,23 @@ 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_3_bounds_warning(self): - points = np.array([2.0]) - bounds = np.array([[1.0, 0.0, 3.0]]) - - coord = AuxCoord(points, bounds=bounds, long_name="x") - - msg = ( - r"Cannot check if coordinate is contiguous: Invalid operation " - r"for 'x', with 3 bound\(s\). Contiguous bounds are only " - r"defined for 1D coordinates with 2 bounds. Metadata may not " - r"be fully descriptive for 'x'. Ignoring bounds." - ) - with warnings.catch_warnings(): - # Cause all warnings to raise Exceptions - warnings.simplefilter("error") - with self.assertRaisesRegex(UserWarning, msg): - coord.collapsed() - - def test_lazy_3_bounds_warning(self): - points = da.arange(1) - bounds = da.arange(1 * 3).reshape(1, 3) - - coord = AuxCoord(points, bounds=bounds, long_name="y") - - msg = ( - r"Cannot check if coordinate is contiguous: Invalid operation " - r"for 'y', with 3 bound\(s\). Contiguous bounds are only " - r"defined for 1D coordinates with 2 bounds. Metadata may not " - r"be fully descriptive for 'y'. Ignoring bounds." - ) - with warnings.catch_warnings(): - # Cause all warnings to raise Exceptions - warnings.simplefilter("error") - with self.assertRaisesRegex(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) + coord = AuxCoord(points, bounds=bounds, long_name="x") - collapsed_coord = coord.collapsed() + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + + msg = ( + "Cannot check if coordinate is contiguous: Invalid operation for " + "'x', with 3 bound(s). Contiguous bounds are only defined for 1D " + "coordinates with 2 bounds. Metadata may not be fully descriptive " + "for 'x'. Ignoring bounds." + ) + self.assertEqual([mock.call(msg)], warn.call_args_list) self.assertFalse(collapsed_coord.has_lazy_points()) self.assertFalse(collapsed_coord.has_lazy_bounds()) @@ -532,9 +505,18 @@ 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) + coord = AuxCoord(points, bounds=bounds, long_name="x") - collapsed_coord = coord.collapsed() + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + + msg = ( + "Cannot check if coordinate is contiguous: Invalid operation for " + "'x', with 3 bound(s). Contiguous bounds are only defined for 1D " + "coordinates with 2 bounds. Metadata may not be fully descriptive " + "for 'x'. Ignoring bounds." + ) + self.assertEqual([mock.call(msg)], warn.call_args_list) self.assertTrue(collapsed_coord.has_lazy_points()) self.assertTrue(collapsed_coord.has_lazy_bounds()) From 54dac3ff517050bce689920cea47ad00013c4a59 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 20 Jul 2022 14:57:43 +0200 Subject: [PATCH 3/6] Added further tests for other warnings --- lib/iris/tests/unit/coords/test_Coord.py | 56 +++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index d8ee57ea63..d55adf35f5 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -332,7 +332,9 @@ def test_dim_1d(self): ) for units in ["unknown", "no_unit", 1, "K"]: coord.units = units - collapsed_coord = coord.collapsed() + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + warn.assert_not_called() self.assertArrayEqual( collapsed_coord.points, np.mean(coord.points) ) @@ -474,6 +476,58 @@ 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") + + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + + msg = ( + "Collapsing a multi-dimensional coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + self.assertEqual([mock.call(msg)], warn.call_args_list) + + def test_lazy_nd_multidim_bounds_warning(self): + self.setupTestArrays((3, 4)) + coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy, long_name="y") + + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + + msg = ( + "Collapsing a multi-dimensional coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + self.assertEqual([mock.call(msg)], warn.call_args_list) + + def test_numeric_nd_noncontiguous_bounds_warning(self): + self.setupTestArrays((3)) + coord = AuxCoord(self.pts_real, bounds=self.bds_real, long_name="y") + + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + + msg = ( + "Collapsing a non-contiguous coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + self.assertEqual([mock.call(msg)], warn.call_args_list) + + def test_lazy_nd_noncontiguous_bounds_warning(self): + self.setupTestArrays((3)) + coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy, long_name="y") + + with mock.patch("warnings.warn") as warn: + collapsed_coord = coord.collapsed() + + msg = ( + "Collapsing a non-contiguous coordinate. " + "Metadata may not be fully descriptive for 'y'." + ) + self.assertEqual([mock.call(msg)], warn.call_args_list) + def test_numeric_3_bounds(self): points = np.array([2.0, 6.0, 4.0]) From 7bc179a2daafe36f48df9d389698fd8656e41aab Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 20 Jul 2022 15:41:47 +0200 Subject: [PATCH 4/6] pre-commit --- lib/iris/tests/unit/coords/test_Coord.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index d55adf35f5..4f7dccc8ff 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -481,7 +481,7 @@ def test_numeric_nd_multidim_bounds_warning(self): coord = AuxCoord(self.pts_real, bounds=self.bds_real, long_name="y") with mock.patch("warnings.warn") as warn: - collapsed_coord = coord.collapsed() + coord.collapsed() msg = ( "Collapsing a multi-dimensional coordinate. " @@ -494,7 +494,7 @@ def test_lazy_nd_multidim_bounds_warning(self): coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy, long_name="y") with mock.patch("warnings.warn") as warn: - collapsed_coord = coord.collapsed() + coord.collapsed() msg = ( "Collapsing a multi-dimensional coordinate. " @@ -507,7 +507,7 @@ def test_numeric_nd_noncontiguous_bounds_warning(self): coord = AuxCoord(self.pts_real, bounds=self.bds_real, long_name="y") with mock.patch("warnings.warn") as warn: - collapsed_coord = coord.collapsed() + coord.collapsed() msg = ( "Collapsing a non-contiguous coordinate. " @@ -520,7 +520,7 @@ def test_lazy_nd_noncontiguous_bounds_warning(self): coord = AuxCoord(self.pts_lazy, bounds=self.bds_lazy, long_name="y") with mock.patch("warnings.warn") as warn: - collapsed_coord = coord.collapsed() + coord.collapsed() msg = ( "Collapsing a non-contiguous coordinate. " From d4e8276b2882a44f53494075464625afdaab0e41 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 24 Aug 2022 18:17:10 +0200 Subject: [PATCH 5/6] Used unittest's assertWarnsRegex --- lib/iris/tests/unit/coords/test_Coord.py | 55 +++++++++--------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 4f7dccc8ff..dca6ed3c1b 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -332,9 +332,8 @@ def test_dim_1d(self): ) for units in ["unknown", "no_unit", 1, "K"]: coord.units = units - with mock.patch("warnings.warn") as warn: + with self.assertNoWarningsRegexp(): collapsed_coord = coord.collapsed() - warn.assert_not_called() self.assertArrayEqual( collapsed_coord.points, np.mean(coord.points) ) @@ -480,53 +479,45 @@ def test_numeric_nd_multidim_bounds_warning(self): self.setupTestArrays((3, 4)) coord = AuxCoord(self.pts_real, bounds=self.bds_real, long_name="y") - with mock.patch("warnings.warn") as warn: - coord.collapsed() - msg = ( "Collapsing a multi-dimensional coordinate. " "Metadata may not be fully descriptive for 'y'." ) - self.assertEqual([mock.call(msg)], warn.call_args_list) + 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") - with mock.patch("warnings.warn") as warn: - coord.collapsed() - msg = ( "Collapsing a multi-dimensional coordinate. " "Metadata may not be fully descriptive for 'y'." ) - self.assertEqual([mock.call(msg)], warn.call_args_list) + 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") - with mock.patch("warnings.warn") as warn: - coord.collapsed() - msg = ( "Collapsing a non-contiguous coordinate. " "Metadata may not be fully descriptive for 'y'." ) - self.assertEqual([mock.call(msg)], warn.call_args_list) + 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") - with mock.patch("warnings.warn") as warn: - coord.collapsed() - msg = ( "Collapsing a non-contiguous coordinate. " "Metadata may not be fully descriptive for 'y'." ) - self.assertEqual([mock.call(msg)], warn.call_args_list) + with self.assertWarnsRegex(UserWarning, msg): + coord.collapsed() def test_numeric_3_bounds(self): @@ -535,16 +526,14 @@ def test_numeric_3_bounds(self): coord = AuxCoord(points, bounds=bounds, long_name="x") - with mock.patch("warnings.warn") as warn: - collapsed_coord = coord.collapsed() - msg = ( - "Cannot check if coordinate is contiguous: Invalid operation for " - "'x', with 3 bound(s). Contiguous bounds are only defined for 1D " - "coordinates with 2 bounds. Metadata may not be fully descriptive " - "for 'x'. Ignoring bounds." + 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." ) - self.assertEqual([mock.call(msg)], warn.call_args_list) + with self.assertWarnsRegex(UserWarning, msg): + collapsed_coord = coord.collapsed() self.assertFalse(collapsed_coord.has_lazy_points()) self.assertFalse(collapsed_coord.has_lazy_bounds()) @@ -561,16 +550,14 @@ def test_lazy_3_bounds(self): coord = AuxCoord(points, bounds=bounds, long_name="x") - with mock.patch("warnings.warn") as warn: - collapsed_coord = coord.collapsed() - msg = ( - "Cannot check if coordinate is contiguous: Invalid operation for " - "'x', with 3 bound(s). Contiguous bounds are only defined for 1D " - "coordinates with 2 bounds. Metadata may not be fully descriptive " - "for 'x'. Ignoring bounds." + 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." ) - self.assertEqual([mock.call(msg)], warn.call_args_list) + with self.assertWarnsRegex(UserWarning, msg): + collapsed_coord = coord.collapsed() self.assertTrue(collapsed_coord.has_lazy_points()) self.assertTrue(collapsed_coord.has_lazy_bounds()) From 4a1fc720c1269b90e8bdaa64fd35aed16b2d1cb9 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Wed, 24 Aug 2022 18:17:49 +0200 Subject: [PATCH 6/6] Added What's new? entry --- docs/src/whatsnew/latest.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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