Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowed collapsing coordinates with nbounds != (0, 2) #4870

Merged
merged 6 commits into from
Aug 25, 2022
Merged
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
7 changes: 6 additions & 1 deletion docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=======================
Expand Down Expand Up @@ -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
.. _NEP18: https://numpy.org/neps/nep-0018-array-function-protocol.html
24 changes: 18 additions & 6 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
95 changes: 94 additions & 1 deletion lib/iris/tests/unit/coords/test_Coord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down Expand Up @@ -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):
Expand Down
61 changes: 61 additions & 0 deletions lib/iris/tests/unit/cube/test_Cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down