From 68d1b9c0784969540d10da4bc042f4eba5202a6d Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 12 Mar 2021 19:36:03 +0000 Subject: [PATCH 1/2] MeshCoord changes : add shape in printout; support __hash__ for use in sets/dicts. --- lib/iris/experimental/ugrid.py | 12 +++++++++++- .../tests/unit/experimental/ugrid/test_MeshCoord.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py index 8326bdccb4..666abbc59c 100644 --- a/lib/iris/experimental/ugrid.py +++ b/lib/iris/experimental/ugrid.py @@ -2896,6 +2896,10 @@ def __eq__(self, other): return eq + # Exactly as for Coord.__hash__ : See there for why. + def __hash__(self): + return hash(id(self)) + def _string_summary(self, repr_style): # Note: bypass the immediate parent here, which is Coord, because we # have no interest in reporting coord_system or climatological, or in @@ -2927,7 +2931,13 @@ def _string_summary(self, repr_style): ) # Add 'other' metadata that is drawn from the underlying node-coord. # But put these *afterward*, unlike other similar classes. - for item in ("standard_name", "units", "long_name", "attributes"): + for item in ( + "shape", + "standard_name", + "units", + "long_name", + "attributes", + ): # NOTE: order of these matches Coord.summary, but omit var_name. val = getattr(self, item, None) if item == "attributes": diff --git a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py index 5a40bdcba0..a0429df611 100644 --- a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py +++ b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py @@ -353,7 +353,7 @@ def _expected_elements_regexp( regexp += r"Mesh\('test_mesh'\)" else: regexp += "" - regexp += ", location='face', axis='x'" + regexp += r", location='face', axis='x', shape=\(3,\)" if standard_name: regexp += ", standard_name='longitude'" regexp += r", units=Unit\('degrees_east'\)" From 0382763f8b1d9061cb887a3ef6702c24ed39d5fb Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 12 Mar 2021 19:37:36 +0000 Subject: [PATCH 2/2] Add mesh_coords key to Cube.coord/coords. With tests. --- lib/iris/cube.py | 43 ++++++++++-- lib/iris/tests/unit/cube/test_Cube.py | 94 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 54f74bab1b..ac9e05f782 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -10,6 +10,7 @@ """ from collections import OrderedDict + from collections.abc import ( Iterable, Container, @@ -1582,6 +1583,7 @@ def coords( dimensions=None, coord_system=None, dim_coords=None, + mesh_coords=None, ): """ Return a list of coordinates from the :class:`Cube` that match the @@ -1643,8 +1645,14 @@ def coords( * dim_coords: Set to ``True`` to only return coordinates that are the cube's dimension coordinates. Set to ``False`` to only return coordinates - that are the cube's auxiliary and derived coordinates. If ``None``, - returns all coordinates. + that are the cube's auxiliary, mesh and derived coordinates. + If ``None``, returns all coordinates. + + * mesh_coords: + Set to ``True`` to return only coordinates which are + :class:`~iris.experimental.ugrid.MeshCoord`\\ s. + Set to ``False`` to return only non-mesh coordinates. + If ``None``, returns all coordinates. Returns: A list containing zero or more coordinates matching the provided @@ -1660,6 +1668,26 @@ def coords( coords_and_factories += list(self.aux_coords) coords_and_factories += list(self.aux_factories) + if mesh_coords is not None: + # Select on mesh or non-mesh. + mesh_coords = bool(mesh_coords) + # Use duck typing to avoid importing from iris.experimental.ugrid, + # which could be a circular import. + if mesh_coords: + # *only* MeshCoords + coords_and_factories = [ + item + for item in coords_and_factories + if hasattr(item, "mesh") + ] + else: + # *not* MeshCoords + coords_and_factories = [ + item + for item in coords_and_factories + if not hasattr(item, "mesh") + ] + coords_and_factories = metadata_filter( coords_and_factories, item=name_or_coord, @@ -1727,6 +1755,7 @@ def coord( dimensions=None, coord_system=None, dim_coords=None, + mesh_coords=None, ): """ Return a single coordinate from the :class:`Cube` that matches the @@ -1793,8 +1822,14 @@ def coord( * dim_coords: Set to ``True`` to only return coordinates that are the cube's dimension coordinates. Set to ``False`` to only return coordinates - that are the cube's auxiliary and derived coordinates. If ``None``, - returns all coordinates. + that are the cube's auxiliary, mesh and derived coordinates. + If ``None``, returns all coordinates. + + * mesh_coords: + Set to ``True`` to return only coordinates which are + :class:`~iris.experimental.ugrid.MeshCoord`\\ s. + Set to ``False`` to return only non-mesh coordinates. + If ``None``, returns all coordinates. Returns: The coordinate that matches the provided criteria. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index ded401cab3..73e9b0bd18 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -25,6 +25,7 @@ from iris.analysis import MEAN from iris.aux_factory import HybridHeightFactory from iris.cube import Cube +from iris.common.metadata import BaseMetadata from iris.coords import ( AuxCoord, DimCoord, @@ -40,6 +41,10 @@ ) from iris._lazy_data import as_lazy_data import iris.tests.stock as stock +from iris.tests.unit.experimental.ugrid.test_MeshCoord import ( + _create_test_mesh as create_test_mesh, + _create_test_meshcoord as create_test_meshcoord, +) class Test___init___data(tests.IrisTest): @@ -1955,6 +1960,95 @@ def test__lazy(self): self._check_copy(cube, cube.copy()) +class Test_coords__mesh_coords(tests.IrisTest): + """ + Checking *only* the new "mesh_coords" keyword of the coord/coords methods. + + This is *not* attached to the existing tests for this area, as they are + very old and patchy legacy tests. See: iris.tests.test_cdm.TestQueryCoord. + + """ + + def setUp(self): + # Create a standard test cube with a variety of types of coord. + mesh = create_test_mesh() + meshx, meshy = ( + create_test_meshcoord(axis=axis, mesh=mesh) for axis in ("x", "y") + ) + + n_faces = meshx.shape[0] + mesh_dimco = DimCoord( + np.arange(n_faces), long_name="i_mesh_face", units="1" + ) + auxco_x = AuxCoord( + np.zeros(n_faces), long_name="mesh_face_aux", units="1" + ) + n_z = 2 + zco = DimCoord(np.arange(n_z), long_name="level", units=1) + cube = Cube(np.zeros((n_z, n_faces)), long_name="mesh_phenom") + cube.add_dim_coord(zco, 0) + cube.add_dim_coord(mesh_dimco, 1) + for co in (meshx, meshy, auxco_x): + cube.add_aux_coord(co, 1) + + self.dimco_z = zco + self.dimco_mesh = mesh_dimco + self.meshco_x = meshx + self.meshco_y = meshy + self.auxco_x = auxco_x + self.allcoords = [meshx, meshy, zco, mesh_dimco, auxco_x] + self.cube = cube + + def _assert_lists_equal(self, items_a, items_b): + """ + Check that two lists of coords, cubes etc contain the same things. + Lists must contain the same items, including any repeats, but can be in + a different order. + + """ + # Compare (and thus sort) by their *common* metadata. + def sortkey(item): + return BaseMetadata.from_metadata(item.metadata) + + items_a = sorted(items_a, key=sortkey) + items_b = sorted(items_b, key=sortkey) + self.assertEqual(items_a, items_b) + + def test_coords__all__meshcoords_default(self): + # coords() includes mesh-coords along with the others. + result = self.cube.coords() + expected = self.allcoords + self._assert_lists_equal(expected, result) + + def test_coords__all__meshcoords_only(self): + # Coords(mesh_coords=True) returns only mesh-coords. + result = self.cube.coords(mesh_coords=True) + expected = [self.meshco_x, self.meshco_y] + self._assert_lists_equal(expected, result) + + def test_coords__all__meshcoords_omitted(self): + # Coords(mesh_coords=False) omits the mesh-coords. + result = self.cube.coords(mesh_coords=False) + expected = set(self.allcoords) - set([self.meshco_x, self.meshco_y]) + self._assert_lists_equal(expected, result) + + def test_coords__axis__meshcoords(self): + # Coord (singular) with axis + mesh_coords=True + result = self.cube.coord(axis="x", mesh_coords=True) + self.assertIs(result, self.meshco_x) + + def test_coords__dimcoords__meshcoords(self): + # dim_coords and mesh_coords should be mutually exclusive. + result = self.cube.coords(dim_coords=True, mesh_coords=True) + self.assertEqual(result, []) + + def test_coords__nodimcoords__meshcoords(self): + # When mesh_coords=True, dim_coords=False should have no effect. + result = self.cube.coords(dim_coords=False, mesh_coords=True) + expected = [self.meshco_x, self.meshco_y] + self._assert_lists_equal(expected, result) + + class Test_dtype(tests.IrisTest): def setUp(self): self.dtypes = (