From fd8ae483bd560801731252b8590c72eb7119c1f4 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Fri, 12 Mar 2021 20:02:24 +0000 Subject: [PATCH 1/7] Add cube mesh+location access methods. --- lib/iris/cube.py | 26 ++++++ lib/iris/tests/unit/cube/test_Cube.py | 113 ++++++++++++++++++++------ 2 files changed, 112 insertions(+), 27 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index ac9e05f782..b22c2d818a 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1919,6 +1919,32 @@ def coord_system(self, spec=None): return result + def mesh(self): + """ + Return the unstructured :class:`~iris.experimental.ugrid.Mesh` + associated with the cube, or None if there is none. + + """ + result = self.coords(mesh_coords=True) + if len(result) == 0: + result = None + else: + result = result[0].mesh + return result + + def location(self): + """ + Return the mesh location of the cube, if the cube has an unstructured + :class:`~iris.experimental.ugrid.Mesh`, or None if there is none. + + """ + result = self.coords(mesh_coords=True) + if len(result) == 0: + result = None + else: + result = result[0].location + return result + def cell_measures(self, name_or_cell_measure=None): """ Return a list of cell measures in this cube fitting the given criteria. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 73e9b0bd18..4d9431006d 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1960,6 +1960,51 @@ def test__lazy(self): self._check_copy(cube, cube.copy()) +def _add_test_meshcube(self, nomesh=False, location="face"): + # A common setup action : Create a standard test cube with a variety of + # types of coord, and add various objects to the testcase, + # i.e. to "self". + if nomesh: + mesh = None + n_faces = 5 + else: + mesh = create_test_mesh() + meshx, meshy = ( + create_test_meshcoord(axis=axis, mesh=mesh, location=location) + 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) + if nomesh: + mesh_coords = [] + else: + mesh_coords = [meshx, meshy] + + cube.add_dim_coord(mesh_dimco, 1) + for co in mesh_coords + [auxco_x]: + cube.add_aux_coord(co, 1) + + self.dimco_z = zco + self.dimco_mesh = mesh_dimco + if not nomesh: + self.meshco_x = meshx + self.meshco_y = meshy + self.auxco_x = auxco_x + self.allcoords = mesh_coords + [zco, mesh_dimco, auxco_x] + self.cube = cube + self.mesh = mesh + + class Test_coords__mesh_coords(tests.IrisTest): """ Checking *only* the new "mesh_coords" keyword of the coord/coords methods. @@ -1971,33 +2016,7 @@ class Test_coords__mesh_coords(tests.IrisTest): 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 + _add_test_meshcube(self) def _assert_lists_equal(self, items_a, items_b): """ @@ -2049,6 +2068,46 @@ def test_coords__nodimcoords__meshcoords(self): self._assert_lists_equal(expected, result) +class Test_mesh(tests.IrisTest): + def setUp(self): + # Create a standard test cube with a variety of types of coord. + _add_test_meshcube(self) + + def test_mesh(self): + result = self.cube.mesh() + self.assertIs(result, self.mesh) + + def test_no_mesh(self): + # Replace standard setUp cube with a no-mesh version. + _add_test_meshcube(self, nomesh=True) + result = self.cube.mesh() + self.assertIsNone(result) + + +class Test_location(tests.IrisTest): + def setUp(self): + # Create a standard test cube with a variety of types of coord. + _add_test_meshcube(self) + + def test_no_mesh(self): + # Replace standard setUp cube with a no-mesh version. + _add_test_meshcube(self, nomesh=True) + result = self.cube.location() + self.assertIsNone(result) + + def test_mesh(self): + cube = self.cube + result = cube.location() + self.assertEqual(result, self.meshco_x.location) + + def test_alternate_location(self): + # Replace standard setUp cube with an edge-based version. + _add_test_meshcube(self, location="edge") + cube = self.cube + result = cube.location() + self.assertEqual(result, "edge") + + class Test_dtype(tests.IrisTest): def setUp(self): self.dtypes = ( From b069a8ed81ac12dafda21ce37bb8fcfb8ca900cd Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 13 Mar 2021 01:31:27 +0000 Subject: [PATCH 2/7] Add tests for Cube.__eq__ with MeshCoords. --- lib/iris/tests/unit/cube/test_Cube.py | 41 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 4d9431006d..50e78786a1 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1960,7 +1960,7 @@ def test__lazy(self): self._check_copy(cube, cube.copy()) -def _add_test_meshcube(self, nomesh=False, location="face"): +def _add_test_meshcube(self, nomesh=False, **meshcoord_kwargs): # A common setup action : Create a standard test cube with a variety of # types of coord, and add various objects to the testcase, # i.e. to "self". @@ -1968,9 +1968,11 @@ def _add_test_meshcube(self, nomesh=False, location="face"): mesh = None n_faces = 5 else: - mesh = create_test_mesh() + mesh = meshcoord_kwargs.pop("mesh", None) + if mesh is None: + mesh = create_test_mesh() meshx, meshy = ( - create_test_meshcoord(axis=axis, mesh=mesh, location=location) + create_test_meshcoord(axis=axis, mesh=mesh, **meshcoord_kwargs) for axis in ("x", "y") ) n_faces = meshx.shape[0] @@ -2108,6 +2110,39 @@ def test_alternate_location(self): self.assertEqual(result, "edge") +class Test__eq__mesh(tests.IrisTest): + """ + Check that cubes with meshes support == as expected. + + Note: there is no special code for this in iris.cube.Cube : it is + provided by the coord comparisons. + + """ + + def setUp(self): + # Create a 'standard' test cube. + _add_test_meshcube(self) + + def test_copied_cube_match(self): + cube = self.cube + cube2 = cube.copy() + self.assertEqual(cube, cube2) + + def test_same_mesh_match(self): + cube1 = self.cube + # re-create an identical cube, using the same mesh. + _add_test_meshcube(self, mesh=self.mesh) + cube2 = self.cube + self.assertEqual(cube1, cube2) + + def test_new_mesh_different(self): + cube1 = self.cube + # re-create an identical cube, using the same mesh. + _add_test_meshcube(self) + cube2 = self.cube + self.assertNotEqual(cube1, cube2) + + class Test_dtype(tests.IrisTest): def setUp(self): self.dtypes = ( From b33072537a8d001c4319e6c6f2054b53704f0983 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Sat, 13 Mar 2021 01:37:26 +0000 Subject: [PATCH 3/7] WIP: enforce compatibility of a cube's MeshCoords. --- lib/iris/cube.py | 19 +++ lib/iris/tests/unit/cube/test_Cube.py | 122 ++++++++++++++++++ .../unit/experimental/ugrid/test_MeshCoord.py | 40 +++--- 3 files changed, 159 insertions(+), 22 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index b22c2d818a..3a0c202680 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1133,6 +1133,25 @@ def _check_multi_dim_metadata(self, metadata, data_dims): def _add_unique_aux_coord(self, coord, data_dims): data_dims = self._check_multi_dim_metadata(coord, data_dims) + if hasattr(coord, "mesh"): + existing_meshcoords = self.coords(mesh_coords=True) + if len(existing_meshcoords) > 0: + co = existing_meshcoords[0] + mesh, location = co.mesh, co.location + if coord.location != location: + msg = ( + f"Location of Meshcoord {coord!r} is " + f"{coord.location!s}, which does not match existing" + f"cube location of {location!s}." + ) + raise ValueError(msg) + if coord.mesh != mesh: + msg = ( + f"Mesh of Meshcoord {coord!r} is " + f"{coord.mesh!r}, which does not match existing" + f"cube mesh of {mesh!r}." + ) + raise ValueError(msg) self._aux_coords_and_dims.append((coord, data_dims)) def add_aux_factory(self, aux_factory): diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 50e78786a1..72aa0bae9a 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2110,6 +2110,128 @@ def test_alternate_location(self): self.assertEqual(result, "edge") +class Test__init__mesh(tests.IrisTest): + """ + Test that creation with mesh-coords functions, and prevents a cube having + incompatible mesh-coords. + + """ + + def setUp(self): + # Create a standard test mesh and other useful components. + mesh = create_test_mesh() + meshco = create_test_meshcoord(mesh=mesh) + self.mesh = mesh + self.meshco = meshco + self.nz = 2 + self.n_faces = meshco.shape[0] + + def test_mesh(self): + # Create a new cube from some of the parts. + nz, n_faces = self.nz, self.n_faces + dimco_z = DimCoord(np.arange(nz), long_name="z") + dimco_mesh = DimCoord(np.arange(n_faces), long_name="x") + meshco = self.meshco + cube = Cube( + np.zeros((nz, n_faces)), + dim_coords_and_dims=[(dimco_z, 0), (dimco_mesh, 1)], + aux_coords_and_dims=[(meshco, 1)], + ) + self.assertEqual(cube.mesh(), meshco.mesh) + + def test_fail_dim_meshcoord(self): + # As "test_mesh", but attempt to use the meshcoord as a dim-coord. + # This should not be allowed. + nz, n_faces = self.nz, self.n_faces + dimco_z = DimCoord(np.arange(nz), long_name="z") + meshco = self.meshco + with self.assertRaisesRegex(ValueError, "may not be an AuxCoord"): + Cube( + np.zeros((nz, n_faces)), + dim_coords_and_dims=[(dimco_z, 0), (meshco, 1)], + ) + + def test_multi_meshcoords(self): + meshco_x = create_test_meshcoord(axis="x", mesh=self.mesh) + meshco_y = create_test_meshcoord(axis="y", mesh=self.mesh) + n_faces = meshco_x.shape[0] + cube = Cube( + np.zeros(n_faces), + aux_coords_and_dims=[(meshco_x, 0), (meshco_y, 0)], + ) + self.assertEqual(cube.mesh(), meshco_x.mesh) + + def test_multi_meshcoords_same_axis(self): + # *Not* an error, as long as the coords are distinguishable. + meshco_1 = create_test_meshcoord(axis="x", mesh=self.mesh) + meshco_2 = create_test_meshcoord(axis="x", mesh=self.mesh) + # Can't make these different at creation, owing to the limited + # constructor args, but we can adjust common metadata afterwards. + meshco_2.rename("junk_name") + + n_faces = meshco_1.shape[0] + cube = Cube( + np.zeros(n_faces), + aux_coords_and_dims=[(meshco_1, 0), (meshco_2, 0)], + ) + self.assertEqual(cube.mesh(), meshco_1.mesh) + + def test_fail_meshcoords_different_locations(self): + # Same as successful 'multi_mesh', but different locations. + # N.B. must have a mesh with n-faces == n-edges to test this + mesh = create_test_mesh(n_faces=7, n_edges=7) + meshco_1 = create_test_meshcoord(axis="x", mesh=mesh, location="face") + meshco_2 = create_test_meshcoord(axis="y", mesh=mesh, location="edge") + # They should still have the same *shape* (or would fail anyway) + self.assertEqual(meshco_1.shape, meshco_2.shape) + n_faces = meshco_1.shape[0] + with self.assertRaisesRegex(ValueError, "Location.* does not match"): + Cube( + np.zeros(n_faces), + aux_coords_and_dims=[(meshco_1, 0), (meshco_2, 0)], + ) + + def test_fail_meshcoords_different_meshes(self): + # Same as above, but not sharing the same mesh. + # This one *is* an error. + # But that could relax in future, if we allow mesh equality testing + # (i.e. "mesh_a == mesh_b" when not "mesh_a is mesh_b") + meshco_x = create_test_meshcoord(axis="x") + meshco_y = create_test_meshcoord(axis="y") # Own (different) mesh + n_faces = meshco_x.shape[0] + with self.assertRaisesRegex(ValueError, "Mesh.* does not match"): + Cube( + np.zeros(n_faces), + aux_coords_and_dims=[(meshco_x, 0), (meshco_y, 0)], + ) + + +class Test__add_aux_coord__mesh(tests.IrisTest): + """ + Test that "Cube.add_aux_coord" functions with a mesh-coord, and prevents a + cube having incompatible mesh-coords. + + """ + + def setUp(self): + self.assertTrue(False) + + +class Test__add_dim_coord__mesh(tests.IrisTest): + """ + Test that "Cube.add_dim_coord" cannot work with a mesh-coord. + + """ + + def test(self): + # Create a mesh with only 2 faces, so coord *can't* be non-monotonic. + mesh = create_test_mesh(n_faces=2) + meshco = create_test_meshcoord(mesh=mesh) + cube = Cube([0, 1]) + with self.assertRaisesRegex(ValueError, "may not be an AuxCoord"): + cube.add_dim_coord(meshco, 0) + + class Test__eq__mesh(tests.IrisTest): """ Check that cubes with meshes support == as expected. diff --git a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py index a0429df611..f7cc1de4b4 100644 --- a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py +++ b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py @@ -38,48 +38,44 @@ _TEST_BOUNDS = _TEST_BOUNDS.reshape((_TEST_N_FACES, _TEST_N_BOUNDS)) -def _create_test_mesh(): +def _create_test_mesh(n_nodes=None, n_faces=None, n_edges=None): + if n_nodes is None: + n_nodes = _TEST_N_NODES + if n_faces is None: + n_faces = _TEST_N_FACES + if n_edges is None: + n_edges = _TEST_N_EDGES node_x = AuxCoord( - 1100 + np.arange(_TEST_N_NODES), + 1100 + np.arange(n_nodes), standard_name="longitude", units="degrees_east", long_name="long-name", var_name="var-name", attributes={"a": 1, "b": "c"}, ) - node_y = AuxCoord( - 1200 + np.arange(_TEST_N_NODES), standard_name="latitude" - ) + node_y = AuxCoord(1200 + np.arange(n_nodes), standard_name="latitude") # Define a rather arbitrary edge-nodes connectivity. # Some nodes are left out, because n_edges*2 < n_nodes. - conns = np.arange(_TEST_N_EDGES * 2, dtype=int) + conns = np.arange(n_edges * 2, dtype=int) # Missing nodes include #0-5, because we add 5. - conns = ((conns + 5) % _TEST_N_NODES).reshape((_TEST_N_EDGES, 2)) + conns = ((conns + 5) % n_nodes).reshape((n_edges, 2)) edge_nodes = Connectivity(conns, cf_role="edge_node_connectivity") - conns = np.arange(_TEST_N_EDGES * 2, dtype=int) + conns = np.arange(n_edges * 2, dtype=int) # Some numbers for the edge coordinates. - edge_x = AuxCoord( - 2100 + np.arange(_TEST_N_EDGES), standard_name="longitude" - ) - edge_y = AuxCoord( - 2200 + np.arange(_TEST_N_EDGES), standard_name="latitude" - ) + edge_x = AuxCoord(2100 + np.arange(n_edges), standard_name="longitude") + edge_y = AuxCoord(2200 + np.arange(n_edges), standard_name="latitude") # Define a rather arbitrary face-nodes connectivity. # Some nodes are left out, because n_faces*n_bounds < n_nodes. - conns = np.arange(_TEST_N_FACES * _TEST_N_BOUNDS, dtype=int) - conns = (conns % _TEST_N_NODES).reshape((_TEST_N_FACES, _TEST_N_BOUNDS)) + conns = np.arange(n_faces * _TEST_N_BOUNDS, dtype=int) + conns = (conns % n_nodes).reshape((n_faces, _TEST_N_BOUNDS)) face_nodes = Connectivity(conns, cf_role="face_node_connectivity") # Some numbers for the edge coordinates. - face_x = AuxCoord( - 3100 + np.arange(_TEST_N_FACES), standard_name="longitude" - ) - face_y = AuxCoord( - 3200 + np.arange(_TEST_N_FACES), standard_name="latitude" - ) + face_x = AuxCoord(3100 + np.arange(n_faces), standard_name="longitude") + face_y = AuxCoord(3200 + np.arange(n_faces), standard_name="latitude") mesh = Mesh( topology_dimension=2, From 15034b0a2ca2725c003ea9e922253d72ea2ddbc6 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 15 Mar 2021 12:27:41 +0000 Subject: [PATCH 4/7] Add Cube.mesh_dim(); ensure all mesh-coords have the same cube dims. --- lib/iris/cube.py | 113 +++++++++++++++++++------- lib/iris/tests/unit/cube/test_Cube.py | 109 +++++++++++++++++++++++-- 2 files changed, 189 insertions(+), 33 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 3a0c202680..37c96c9af2 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1134,24 +1134,43 @@ def _check_multi_dim_metadata(self, metadata, data_dims): def _add_unique_aux_coord(self, coord, data_dims): data_dims = self._check_multi_dim_metadata(coord, data_dims) if hasattr(coord, "mesh"): - existing_meshcoords = self.coords(mesh_coords=True) - if len(existing_meshcoords) > 0: - co = existing_meshcoords[0] - mesh, location = co.mesh, co.location + mesh = self.mesh() + if mesh: + msg = ( + "{item} of Meshcoord {coord!r} is " + "{thisval!r}, which does not match existing " + "cube {item} of {ownval!r}." + ) + if coord.mesh != mesh: + raise ValueError( + msg.format( + item="mesh", + coord=coord, + thisval=coord.mesh, + ownval=mesh, + ) + ) + location = self.location() if coord.location != location: - msg = ( - f"Location of Meshcoord {coord!r} is " - f"{coord.location!s}, which does not match existing" - f"cube location of {location!s}." + raise ValueError( + msg.format( + item="location", + coord=coord, + thisval=coord.location, + ownval=location, + ) ) - raise ValueError(msg) - if coord.mesh != mesh: - msg = ( - f"Mesh of Meshcoord {coord!r} is " - f"{coord.mesh!r}, which does not match existing" - f"cube mesh of {mesh!r}." + mesh_dims = (self.mesh_dim(),) + if data_dims != mesh_dims: + raise ValueError( + msg.format( + item="mesh dimension", + coord=coord, + thisval=data_dims, + ownval=mesh_dims, + ) ) - raise ValueError(msg) + self._aux_coords_and_dims.append((coord, data_dims)) def add_aux_factory(self, aux_factory): @@ -1938,30 +1957,68 @@ def coord_system(self, spec=None): return result + def _a_meshcoord(self): + mesh_coords = self.coords(mesh_coords=True) + if mesh_coords: + result = mesh_coords[0] + else: + result = None + return result + def mesh(self): """ Return the unstructured :class:`~iris.experimental.ugrid.Mesh` - associated with the cube, or None if there is none. + associated with the cube, if the cube has any + :class:`~iris.experimental.ugrid.MeshCoord`\\ s, + or None if it has none. + + Returns: + * mesh (:class:`iris.experimental.ugrid.Mesh` or None) + The mesh of the cube + :class:`~iris.experimental.ugrid.MeshCoord`\\s, + or None. """ - result = self.coords(mesh_coords=True) - if len(result) == 0: - result = None - else: - result = result[0].mesh + result = self._a_meshcoord() + if result is not None: + result = result.mesh return result def location(self): """ - Return the mesh location of the cube, if the cube has an unstructured - :class:`~iris.experimental.ugrid.Mesh`, or None if there is none. + Return the "location" of the cube mesh, if the cube has any + :class:`~iris.experimental.ugrid.MeshCoord`\\ s, + or None if it has none. + + Returns: + * location (str or None) + The mesh location of the cube + :class:`~iris.experimental.ugrid.MeshCoord`\\s, + one of 'face' / 'edge' / 'node', + or None. """ - result = self.coords(mesh_coords=True) - if len(result) == 0: - result = None - else: - result = result[0].location + result = self._a_meshcoord() + if result is not None: + result = result.location + return result + + def mesh_dim(self): + """ + Return the cube dimension of the mesh, if the cube has any + :class:`~iris.experimental.ugrid.MeshCoord`\\ s, + or None if it has none. + + Returns: + * mesh_dim (int, or None) + the cube dimension which the cube + :class:`~iris.experimental.ugrid.MeshCoord`\\s map to, + or None. + + """ + result = self._a_meshcoord() + if result is not None: + (result,) = self.coord_dims(result) # result is a 1-tuple return result def cell_measures(self, name_or_cell_measure=None): diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 72aa0bae9a..f87b4ccfe7 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1960,7 +1960,7 @@ def test__lazy(self): self._check_copy(cube, cube.copy()) -def _add_test_meshcube(self, nomesh=False, **meshcoord_kwargs): +def _add_test_meshcube(self, nomesh=False, n_z=2, **meshcoord_kwargs): # A common setup action : Create a standard test cube with a variety of # types of coord, and add various objects to the testcase, # i.e. to "self". @@ -1983,7 +1983,6 @@ def _add_test_meshcube(self, nomesh=False, **meshcoord_kwargs): 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) @@ -2110,6 +2109,32 @@ def test_alternate_location(self): self.assertEqual(result, "edge") +class Test_mesh_dim(tests.IrisTest): + def setUp(self): + # Create a standard test cube with a variety of types of coord. + _add_test_meshcube(self) + + def test_no_mesh(self): + # Replace standard setUp cube with a no-mesh version. + _add_test_meshcube(self, nomesh=True) + result = self.cube.mesh_dim() + self.assertIsNone(result) + + def test_mesh(self): + cube = self.cube + result = cube.mesh_dim() + self.assertEqual(result, 1) + + def test_alternate(self): + # Replace standard setUp cube with an edge-based version. + _add_test_meshcube(self, location="edge") + cube = self.cube + # Transpose the cube : the mesh dim is then 0 + cube.transpose() + result = cube.mesh_dim() + self.assertEqual(result, 0) + + class Test__init__mesh(tests.IrisTest): """ Test that creation with mesh-coords functions, and prevents a cube having @@ -2185,14 +2210,15 @@ def test_fail_meshcoords_different_locations(self): # They should still have the same *shape* (or would fail anyway) self.assertEqual(meshco_1.shape, meshco_2.shape) n_faces = meshco_1.shape[0] - with self.assertRaisesRegex(ValueError, "Location.* does not match"): + msg = "does not match existing cube location" + with self.assertRaisesRegex(ValueError, msg): Cube( np.zeros(n_faces), aux_coords_and_dims=[(meshco_1, 0), (meshco_2, 0)], ) def test_fail_meshcoords_different_meshes(self): - # Same as above, but not sharing the same mesh. + # Same as successful 'multi_mesh', but not sharing the same mesh. # This one *is* an error. # But that could relax in future, if we allow mesh equality testing # (i.e. "mesh_a == mesh_b" when not "mesh_a is mesh_b") @@ -2205,6 +2231,20 @@ def test_fail_meshcoords_different_meshes(self): aux_coords_and_dims=[(meshco_x, 0), (meshco_y, 0)], ) + def test_fail_meshcoords_different_dims(self): + # Same as 'test_mesh', but meshcoords on different dimensions. + # Replace standard setup with one where n_z == n_faces. + n_z, n_faces = 4, 4 + mesh = create_test_mesh(n_faces=n_faces) + meshco_x = create_test_meshcoord(mesh=mesh, axis="x") + meshco_y = create_test_meshcoord(mesh=mesh, axis="y") + msg = "does not match existing cube mesh dimension" + with self.assertRaisesRegex(ValueError, msg): + Cube( + np.zeros((n_z, n_faces)), + aux_coords_and_dims=[(meshco_x, 1), (meshco_y, 0)], + ) + class Test__add_aux_coord__mesh(tests.IrisTest): """ @@ -2214,7 +2254,66 @@ class Test__add_aux_coord__mesh(tests.IrisTest): """ def setUp(self): - self.assertTrue(False) + _add_test_meshcube(self) + # Remove the existing "meshco_y", so we can add similar ones without + # needing to distinguish from the existing. + self.cube.remove_coord(self.meshco_y) + + def test_add_compatible(self): + cube = self.cube + meshco_y = self.meshco_y + # Add the y-meshco back into the cube. + cube.add_aux_coord(meshco_y, 1) + self.assertIn(meshco_y, cube.coords(mesh_coords=True)) + + def test_add_multiple(self): + # Show that we can add extra mesh coords. + cube = self.cube + meshco_y = self.meshco_y + # Add the y-meshco back into the cube. + cube.add_aux_coord(meshco_y, 1) + # Make a duplicate y-meshco, renamed so it can add into the cube. + new_meshco_y = meshco_y.copy() + new_meshco_y.rename("alternative") + cube.add_aux_coord(new_meshco_y, 1) + self.assertEqual(len(cube.coords(mesh_coords=True)), 3) + + def test_fail_different_mesh(self): + # Make a duplicate y-meshco, and rename so it can add into the cube. + cube = self.cube + # Create 'meshco_y' duplicate, but a new mesh + meshco_y = create_test_meshcoord(axis="y") + msg = "does not match existing cube mesh" + with self.assertRaisesRegex(ValueError, msg): + cube.add_aux_coord(meshco_y, 1) + + def test_fail_different_location(self): + # Make a new mesh with equal n_faces and n_edges + mesh = create_test_mesh(n_faces=4, n_edges=4) + # Re-make the test objects based on that. + _add_test_meshcube(self, mesh=mesh) + cube = self.cube + cube.remove_coord(self.meshco_y) # Remove y-coord, as in setUp() + # Create a new meshco_y, same mesh but based on edges. + meshco_y = create_test_meshcoord( + axis="y", mesh=self.mesh, location="edge" + ) + msg = "does not match existing cube location" + with self.assertRaisesRegex(ValueError, msg): + cube.add_aux_coord(meshco_y, 1) + + def test_fail_different_dimension(self): + # Re-make the test objects with the non-mesh dim equal in length. + n_faces = self.cube.shape[1] + _add_test_meshcube(self, n_z=n_faces) + cube = self.cube + meshco_y = self.meshco_y + cube.remove_coord(meshco_y) # Remove y-coord, as in setUp() + + # Attempt to re-attach the 'y' meshcoord, to a different cube dimension. + msg = "does not match existing cube mesh dimension" + with self.assertRaisesRegex(ValueError, msg): + cube.add_aux_coord(meshco_y, 0) class Test__add_dim_coord__mesh(tests.IrisTest): From 944e731e41c3d44dac733a2730535c3a7a2fa1e4 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 15 Mar 2021 14:54:48 +0000 Subject: [PATCH 5/7] Docstring fixes and improvements. --- lib/iris/cube.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 37c96c9af2..afcdfb8888 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1970,13 +1970,14 @@ def mesh(self): Return the unstructured :class:`~iris.experimental.ugrid.Mesh` associated with the cube, if the cube has any :class:`~iris.experimental.ugrid.MeshCoord`\\ s, - or None if it has none. + or ``None`` if it has none. Returns: - * mesh (:class:`iris.experimental.ugrid.Mesh` or None) + + * mesh (:class:`iris.experimental.ugrid.Mesh` or None): The mesh of the cube :class:`~iris.experimental.ugrid.MeshCoord`\\s, - or None. + or ``None``. """ result = self._a_meshcoord() @@ -1986,16 +1987,17 @@ def mesh(self): def location(self): """ - Return the "location" of the cube mesh, if the cube has any + Return the mesh "location" of the cube data, if the cube has any :class:`~iris.experimental.ugrid.MeshCoord`\\ s, - or None if it has none. + or ``None`` if it has none. Returns: - * location (str or None) + + * location (str or None): The mesh location of the cube - :class:`~iris.experimental.ugrid.MeshCoord`\\s, - one of 'face' / 'edge' / 'node', - or None. + :class:`~iris.experimental.ugrid.MeshCoord`\\s + (i.e. one of 'face' / 'edge' / 'node'), + or ``None``. """ result = self._a_meshcoord() @@ -2007,13 +2009,14 @@ def mesh_dim(self): """ Return the cube dimension of the mesh, if the cube has any :class:`~iris.experimental.ugrid.MeshCoord`\\ s, - or None if it has none. + or ``None`` if it has none. Returns: - * mesh_dim (int, or None) + + * mesh_dim (int, or None): the cube dimension which the cube :class:`~iris.experimental.ugrid.MeshCoord`\\s map to, - or None. + or ``None``. """ result = self._a_meshcoord() From af08c5a8b470fbaafda55b4560a7a3992e813fa5 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 17 Mar 2021 10:49:07 +0000 Subject: [PATCH 6/7] Factor out test-mesh-cube creation into iris.tests.stock. --- lib/iris/tests/stock/mesh.py | 159 +++++++++++++++++ lib/iris/tests/unit/cube/test_Cube.py | 92 ++++------ .../unit/experimental/ugrid/test_MeshCoord.py | 167 +++++------------- 3 files changed, 237 insertions(+), 181 deletions(-) create mode 100644 lib/iris/tests/stock/mesh.py diff --git a/lib/iris/tests/stock/mesh.py b/lib/iris/tests/stock/mesh.py new file mode 100644 index 0000000000..f3ce783fad --- /dev/null +++ b/lib/iris/tests/stock/mesh.py @@ -0,0 +1,159 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Helper functions making objects for unstructured mesh testing.""" + + +import numpy as np + +from iris.coords import AuxCoord, DimCoord +from iris.cube import Cube +from iris.experimental.ugrid import Connectivity, Mesh, MeshCoord + +# Default creation controls for creating a test Mesh. +# Note: we're not creating any kind of sensible 'normal' mesh here, the numbers +# of nodes/faces/edges are quite arbitrary and the connectivities we generate +# are pretty random too. +_TEST_N_NODES = 15 +_TEST_N_FACES = 3 +_TEST_N_EDGES = 5 +_TEST_N_BOUNDS = 4 + + +def sample_mesh(n_nodes=None, n_faces=None, n_edges=None): + """ + Make a test mesh. + + Mesh has faces edges, face-coords and edge-coords, numbers of which can be controlled. + + """ + if n_nodes is None: + n_nodes = _TEST_N_NODES + if n_faces is None: + n_faces = _TEST_N_FACES + if n_edges is None: + n_edges = _TEST_N_EDGES + node_x = AuxCoord( + 1100 + np.arange(n_nodes), + standard_name="longitude", + units="degrees_east", + long_name="long-name", + var_name="var-name", + attributes={"a": 1, "b": "c"}, + ) + node_y = AuxCoord(1200 + np.arange(n_nodes), standard_name="latitude") + + # Define a rather arbitrary edge-nodes connectivity. + # Some nodes are left out, because n_edges*2 < n_nodes. + conns = np.arange(n_edges * 2, dtype=int) + # Missing nodes include #0-5, because we add 5. + conns = ((conns + 5) % n_nodes).reshape((n_edges, 2)) + edge_nodes = Connectivity(conns, cf_role="edge_node_connectivity") + conns = np.arange(n_edges * 2, dtype=int) + + # Some numbers for the edge coordinates. + edge_x = AuxCoord(2100 + np.arange(n_edges), standard_name="longitude") + edge_y = AuxCoord(2200 + np.arange(n_edges), standard_name="latitude") + + # Define a rather arbitrary face-nodes connectivity. + # Some nodes are left out, because n_faces*n_bounds < n_nodes. + conns = np.arange(n_faces * _TEST_N_BOUNDS, dtype=int) + conns = (conns % n_nodes).reshape((n_faces, _TEST_N_BOUNDS)) + face_nodes = Connectivity(conns, cf_role="face_node_connectivity") + + # Some numbers for the edge coordinates. + face_x = AuxCoord(3100 + np.arange(n_faces), standard_name="longitude") + face_y = AuxCoord(3200 + np.arange(n_faces), standard_name="latitude") + + mesh = Mesh( + topology_dimension=2, + node_coords_and_axes=[(node_x, "x"), (node_y, "y")], + connectivities=[face_nodes, edge_nodes], + edge_coords_and_axes=[(edge_x, "x"), (edge_y, "y")], + face_coords_and_axes=[(face_x, "x"), (face_y, "y")], + ) + return mesh + + +def sample_meshcoord(mesh=None, location="face", axis="x", **extra_kwargs): + """ + Create a test MeshCoord. + + The creation args are defaulted, including the mesh. + If not provided as an arg, a new mesh is created with sample_mesh(). + + """ + if mesh is None: + mesh = sample_mesh() + result = MeshCoord(mesh=mesh, location=location, axis=axis, **extra_kwargs) + return result + + +def sample_mesh_cube( + nomesh=False, n_z=2, with_parts=False, **meshcoord_kwargs +): + """ + Create a 2d test cube with 1 'normal' and 1 unstructured dimension (with a Mesh). + + Result contains : dimcoords for both dims; an auxcoord on the unstructured dim; 2 mesh-coords. + By default, the mesh is provided by :func:`sample_mesh`, so coordinates and connectivity are not realistic. + + Kwargs: + * nomesh(bool): + If set, don't add MeshCoords, so dim 1 is just a plain anonymous dim. + * n_z (int): + Length of the 'normal' dim. If 0, it is *omitted*. + * with_parts (bool): + If set, return all the constituent component coords + * meshcoord_kwargs (dict): + Extra controls passed to :func:`sample_meshcoord` for MeshCoord creation, to allow user-specified + location/mesh. The 'axis' key is not available, as we always add both an 'x' and 'y' MeshCOord. + + Returns: + * cube : if with_parts not set + * (cube, parts) : if with_parts is set + 'parts' is (mesh, dim0-dimcoord, dim1-dimcoord, dim1-auxcoord, x-meshcoord [or None], y-meshcoord [or None]). + + """ + if nomesh: + mesh = None + n_faces = 5 + else: + mesh = meshcoord_kwargs.pop("mesh", None) + if mesh is None: + mesh = sample_mesh() + meshx, meshy = ( + sample_meshcoord(axis=axis, mesh=mesh, **meshcoord_kwargs) + 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") + + 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) + if nomesh: + mesh_coords = [] + else: + mesh_coords = [meshx, meshy] + + cube.add_dim_coord(mesh_dimco, 1) + for co in mesh_coords + [auxco_x]: + cube.add_aux_coord(co, 1) + + if not with_parts: + result = cube + else: + if nomesh: + meshx, meshy = None, None + parts = (mesh, zco, mesh_dimco, auxco_x, meshx, meshy) + result = (cube, parts) + + return result diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index f87b4ccfe7..e273e2edde 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -41,9 +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, +from iris.tests.stock.mesh import ( + sample_mesh, + sample_meshcoord, + sample_mesh_cube, ) @@ -1961,49 +1962,24 @@ def test__lazy(self): def _add_test_meshcube(self, nomesh=False, n_z=2, **meshcoord_kwargs): - # A common setup action : Create a standard test cube with a variety of - # types of coord, and add various objects to the testcase, - # i.e. to "self". - if nomesh: - mesh = None - n_faces = 5 - else: - mesh = meshcoord_kwargs.pop("mesh", None) - if mesh is None: - mesh = create_test_mesh() - meshx, meshy = ( - create_test_meshcoord(axis=axis, mesh=mesh, **meshcoord_kwargs) - 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") - - 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) - if nomesh: - mesh_coords = [] - else: - mesh_coords = [meshx, meshy] - - cube.add_dim_coord(mesh_dimco, 1) - for co in mesh_coords + [auxco_x]: - cube.add_aux_coord(co, 1) + """ + Common setup action : Create a standard mesh test cube with a variety of coords, and save the cube and various of + its components as properties of the 'self' TestCase. + """ + cube, parts = sample_mesh_cube( + nomesh=nomesh, n_z=n_z, with_parts=True, **meshcoord_kwargs + ) + mesh, zco, mesh_dimco, auxco_x, meshx, meshy = parts + self.mesh = mesh self.dimco_z = zco self.dimco_mesh = mesh_dimco if not nomesh: self.meshco_x = meshx self.meshco_y = meshy self.auxco_x = auxco_x - self.allcoords = mesh_coords + [zco, mesh_dimco, auxco_x] + self.allcoords = [meshx, meshy, zco, mesh_dimco, auxco_x] self.cube = cube - self.mesh = mesh class Test_coords__mesh_coords(tests.IrisTest): @@ -2144,8 +2120,8 @@ class Test__init__mesh(tests.IrisTest): def setUp(self): # Create a standard test mesh and other useful components. - mesh = create_test_mesh() - meshco = create_test_meshcoord(mesh=mesh) + mesh = sample_mesh() + meshco = sample_meshcoord(mesh=mesh) self.mesh = mesh self.meshco = meshco self.nz = 2 @@ -2177,8 +2153,8 @@ def test_fail_dim_meshcoord(self): ) def test_multi_meshcoords(self): - meshco_x = create_test_meshcoord(axis="x", mesh=self.mesh) - meshco_y = create_test_meshcoord(axis="y", mesh=self.mesh) + meshco_x = sample_meshcoord(axis="x", mesh=self.mesh) + meshco_y = sample_meshcoord(axis="y", mesh=self.mesh) n_faces = meshco_x.shape[0] cube = Cube( np.zeros(n_faces), @@ -2188,8 +2164,8 @@ def test_multi_meshcoords(self): def test_multi_meshcoords_same_axis(self): # *Not* an error, as long as the coords are distinguishable. - meshco_1 = create_test_meshcoord(axis="x", mesh=self.mesh) - meshco_2 = create_test_meshcoord(axis="x", mesh=self.mesh) + meshco_1 = sample_meshcoord(axis="x", mesh=self.mesh) + meshco_2 = sample_meshcoord(axis="x", mesh=self.mesh) # Can't make these different at creation, owing to the limited # constructor args, but we can adjust common metadata afterwards. meshco_2.rename("junk_name") @@ -2204,9 +2180,9 @@ def test_multi_meshcoords_same_axis(self): def test_fail_meshcoords_different_locations(self): # Same as successful 'multi_mesh', but different locations. # N.B. must have a mesh with n-faces == n-edges to test this - mesh = create_test_mesh(n_faces=7, n_edges=7) - meshco_1 = create_test_meshcoord(axis="x", mesh=mesh, location="face") - meshco_2 = create_test_meshcoord(axis="y", mesh=mesh, location="edge") + mesh = sample_mesh(n_faces=7, n_edges=7) + meshco_1 = sample_meshcoord(axis="x", mesh=mesh, location="face") + meshco_2 = sample_meshcoord(axis="y", mesh=mesh, location="edge") # They should still have the same *shape* (or would fail anyway) self.assertEqual(meshco_1.shape, meshco_2.shape) n_faces = meshco_1.shape[0] @@ -2222,8 +2198,8 @@ def test_fail_meshcoords_different_meshes(self): # This one *is* an error. # But that could relax in future, if we allow mesh equality testing # (i.e. "mesh_a == mesh_b" when not "mesh_a is mesh_b") - meshco_x = create_test_meshcoord(axis="x") - meshco_y = create_test_meshcoord(axis="y") # Own (different) mesh + meshco_x = sample_meshcoord(axis="x") + meshco_y = sample_meshcoord(axis="y") # Own (different) mesh n_faces = meshco_x.shape[0] with self.assertRaisesRegex(ValueError, "Mesh.* does not match"): Cube( @@ -2235,9 +2211,9 @@ def test_fail_meshcoords_different_dims(self): # Same as 'test_mesh', but meshcoords on different dimensions. # Replace standard setup with one where n_z == n_faces. n_z, n_faces = 4, 4 - mesh = create_test_mesh(n_faces=n_faces) - meshco_x = create_test_meshcoord(mesh=mesh, axis="x") - meshco_y = create_test_meshcoord(mesh=mesh, axis="y") + mesh = sample_mesh(n_faces=n_faces) + meshco_x = sample_meshcoord(mesh=mesh, axis="x") + meshco_y = sample_meshcoord(mesh=mesh, axis="y") msg = "does not match existing cube mesh dimension" with self.assertRaisesRegex(ValueError, msg): Cube( @@ -2282,22 +2258,20 @@ def test_fail_different_mesh(self): # Make a duplicate y-meshco, and rename so it can add into the cube. cube = self.cube # Create 'meshco_y' duplicate, but a new mesh - meshco_y = create_test_meshcoord(axis="y") + meshco_y = sample_meshcoord(axis="y") msg = "does not match existing cube mesh" with self.assertRaisesRegex(ValueError, msg): cube.add_aux_coord(meshco_y, 1) def test_fail_different_location(self): # Make a new mesh with equal n_faces and n_edges - mesh = create_test_mesh(n_faces=4, n_edges=4) + mesh = sample_mesh(n_faces=4, n_edges=4) # Re-make the test objects based on that. _add_test_meshcube(self, mesh=mesh) cube = self.cube cube.remove_coord(self.meshco_y) # Remove y-coord, as in setUp() # Create a new meshco_y, same mesh but based on edges. - meshco_y = create_test_meshcoord( - axis="y", mesh=self.mesh, location="edge" - ) + meshco_y = sample_meshcoord(axis="y", mesh=self.mesh, location="edge") msg = "does not match existing cube location" with self.assertRaisesRegex(ValueError, msg): cube.add_aux_coord(meshco_y, 1) @@ -2324,8 +2298,8 @@ class Test__add_dim_coord__mesh(tests.IrisTest): def test(self): # Create a mesh with only 2 faces, so coord *can't* be non-monotonic. - mesh = create_test_mesh(n_faces=2) - meshco = create_test_meshcoord(mesh=mesh) + mesh = sample_mesh(n_faces=2) + meshco = sample_meshcoord(mesh=mesh) cube = Cube([0, 1]) with self.assertRaisesRegex(ValueError, "may not be an AuxCoord"): cube.add_dim_coord(meshco, 0) diff --git a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py index f7cc1de4b4..df5caecb8b 100644 --- a/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py +++ b/lib/iris/tests/unit/experimental/ugrid/test_MeshCoord.py @@ -20,99 +20,23 @@ from iris.cube import Cube from iris.experimental.ugrid import Connectivity, Mesh from iris._lazy_data import is_lazy_data +import iris.tests.stock.mesh +from iris.tests.stock.mesh import sample_mesh, sample_meshcoord from iris.experimental.ugrid import MeshCoord -# Default creation controls for creating a test Mesh. -# Note: we're not creating any kind of sensible 'normal' mesh here, the numbers -# of nodes/faces/edges are quite arbitrary and the connectivities we generate -# are pretty random too. -_TEST_N_NODES = 15 -_TEST_N_FACES = 3 -_TEST_N_EDGES = 5 -_TEST_N_BOUNDS = 4 - -# Default actual points + bounds. -_TEST_POINTS = np.arange(_TEST_N_FACES) -_TEST_BOUNDS = np.arange(_TEST_N_FACES * _TEST_N_BOUNDS) -_TEST_BOUNDS = _TEST_BOUNDS.reshape((_TEST_N_FACES, _TEST_N_BOUNDS)) - - -def _create_test_mesh(n_nodes=None, n_faces=None, n_edges=None): - if n_nodes is None: - n_nodes = _TEST_N_NODES - if n_faces is None: - n_faces = _TEST_N_FACES - if n_edges is None: - n_edges = _TEST_N_EDGES - node_x = AuxCoord( - 1100 + np.arange(n_nodes), - standard_name="longitude", - units="degrees_east", - long_name="long-name", - var_name="var-name", - attributes={"a": 1, "b": "c"}, - ) - node_y = AuxCoord(1200 + np.arange(n_nodes), standard_name="latitude") - - # Define a rather arbitrary edge-nodes connectivity. - # Some nodes are left out, because n_edges*2 < n_nodes. - conns = np.arange(n_edges * 2, dtype=int) - # Missing nodes include #0-5, because we add 5. - conns = ((conns + 5) % n_nodes).reshape((n_edges, 2)) - edge_nodes = Connectivity(conns, cf_role="edge_node_connectivity") - conns = np.arange(n_edges * 2, dtype=int) - - # Some numbers for the edge coordinates. - edge_x = AuxCoord(2100 + np.arange(n_edges), standard_name="longitude") - edge_y = AuxCoord(2200 + np.arange(n_edges), standard_name="latitude") - - # Define a rather arbitrary face-nodes connectivity. - # Some nodes are left out, because n_faces*n_bounds < n_nodes. - conns = np.arange(n_faces * _TEST_N_BOUNDS, dtype=int) - conns = (conns % n_nodes).reshape((n_faces, _TEST_N_BOUNDS)) - face_nodes = Connectivity(conns, cf_role="face_node_connectivity") - - # Some numbers for the edge coordinates. - face_x = AuxCoord(3100 + np.arange(n_faces), standard_name="longitude") - face_y = AuxCoord(3200 + np.arange(n_faces), standard_name="latitude") - - mesh = Mesh( - topology_dimension=2, - node_coords_and_axes=[(node_x, "x"), (node_y, "y")], - connectivities=[face_nodes, edge_nodes], - edge_coords_and_axes=[(edge_x, "x"), (edge_y, "y")], - face_coords_and_axes=[(face_x, "x"), (face_y, "y")], - ) - return mesh - - -def _default_create_args(): - # Produce a minimal set of default constructor args - kwargs = {"location": "face", "axis": "x", "mesh": _create_test_mesh()} - # NOTE: *don't* include coord_system or climatology. - # We expect to only set those (non-default) explicitly. - return kwargs - - -def _create_test_meshcoord(**override_kwargs): - kwargs = _default_create_args() - # Apply requested overrides and additions. - kwargs.update(override_kwargs) - # Create and return the test coord. - result = MeshCoord(**kwargs) - return result - class Test___init__(tests.IrisTest): def setUp(self): - self.meshcoord = _create_test_meshcoord() + mesh = sample_mesh() + self.mesh = mesh + self.meshcoord = sample_meshcoord(mesh=mesh) def test_basic(self): - kwargs = _default_create_args() - meshcoord = _create_test_meshcoord(**kwargs) - for key, val in kwargs.items(): - self.assertEqual(getattr(meshcoord, key), val) + meshcoord = self.meshcoord + self.assertEqual(meshcoord.mesh, self.mesh) + self.assertEqual(meshcoord.location, "face") + self.assertEqual(meshcoord.axis, "x") self.assertIsInstance(meshcoord, MeshCoord) self.assertIsInstance(meshcoord, Coord) @@ -120,7 +44,7 @@ def test_derived_properties(self): # Check the derived properties of the meshcoord against the correct # underlying mesh coordinate. for axis in Mesh.AXES: - meshcoord = _create_test_meshcoord(axis=axis) + meshcoord = sample_meshcoord(axis=axis) # N.B. node_x_coord = meshcoord.mesh.coord(include_nodes=True, axis=axis) for key in node_x_coord.metadata._fields: @@ -134,25 +58,25 @@ def test_derived_properties(self): def test_fail_bad_mesh(self): with self.assertRaisesRegex(TypeError, "must be a.*Mesh"): - _create_test_meshcoord(mesh=mock.sentinel.odd) + sample_meshcoord(mesh=mock.sentinel.odd) def test_valid_locations(self): for loc in Mesh.LOCATIONS: - meshcoord = _create_test_meshcoord(location=loc) + meshcoord = sample_meshcoord(location=loc) self.assertEqual(meshcoord.location, loc) def test_fail_bad_location(self): with self.assertRaisesRegex(ValueError, "not a valid Mesh location"): - _create_test_meshcoord(location="bad") + sample_meshcoord(location="bad") def test_fail_bad_axis(self): with self.assertRaisesRegex(ValueError, "not a valid Mesh axis"): - _create_test_meshcoord(axis="q") + sample_meshcoord(axis="q") class Test__readonly_properties(tests.IrisTest): def setUp(self): - self.meshcoord = _create_test_meshcoord() + self.meshcoord = sample_meshcoord() def test_fixed_metadata(self): # Check that you cannot set any of these on an existing MeshCoord. @@ -188,7 +112,7 @@ class Test__inherited_properties(tests.IrisTest): """ def setUp(self): - self.meshcoord = _create_test_meshcoord() + self.meshcoord = sample_meshcoord() def test_inherited_properties(self): # Check that these are settable, and affect equality. @@ -211,14 +135,15 @@ class Test__points_and_bounds(tests.IrisTest): # Basic method testing only, for 3 locations with simple array values. # See Test_MeshCoord__dataviews for more detailed checks. def test_node(self): - meshcoord = _create_test_meshcoord(location="node") + meshcoord = sample_meshcoord(location="node") + n_nodes = ( + iris.tests.stock.mesh._TEST_N_NODES + ) # n-nodes default for sample mesh self.assertIsNone(meshcoord.core_bounds()) - self.assertArrayAllClose( - meshcoord.points, 1100 + np.arange(_TEST_N_NODES) - ) + self.assertArrayAllClose(meshcoord.points, 1100 + np.arange(n_nodes)) def test_edge(self): - meshcoord = _create_test_meshcoord(location="edge") + meshcoord = sample_meshcoord(location="edge") points, bounds = meshcoord.core_points(), meshcoord.core_bounds() self.assertEqual(points.shape, meshcoord.shape) self.assertEqual(bounds.shape, meshcoord.shape + (2,)) @@ -237,7 +162,7 @@ def test_edge(self): ) def test_face(self): - meshcoord = _create_test_meshcoord(location="face") + meshcoord = sample_meshcoord(location="face") points, bounds = meshcoord.core_points(), meshcoord.core_bounds() self.assertEqual(points.shape, meshcoord.shape) self.assertEqual(bounds.shape, meshcoord.shape + (4,)) @@ -254,10 +179,10 @@ def test_face(self): class Test___eq__(tests.IrisTest): def setUp(self): - self.mesh = _create_test_mesh() + self.mesh = sample_mesh() def _create_common_mesh(self, **kwargs): - return _create_test_meshcoord(mesh=self.mesh, **kwargs) + return sample_meshcoord(mesh=self.mesh, **kwargs) def test_same_mesh(self): meshcoord1 = self._create_common_mesh() @@ -266,10 +191,10 @@ def test_same_mesh(self): def test_different_identical_mesh(self): # For equality, must have the SAME mesh (at present). - mesh1 = _create_test_mesh() - mesh2 = _create_test_mesh() # Presumably identical, but not the same - meshcoord1 = _create_test_meshcoord(mesh=mesh1) - meshcoord2 = _create_test_meshcoord(mesh=mesh2) + mesh1 = sample_mesh() + mesh2 = sample_mesh() # Presumably identical, but not the same + meshcoord1 = sample_meshcoord(mesh=mesh1) + meshcoord2 = sample_meshcoord(mesh=mesh2) # These should NOT compare, because the Meshes are not identical : at # present, Mesh equality is not implemented (i.e. limited to identity) self.assertNotEqual(meshcoord2, meshcoord1) @@ -287,7 +212,7 @@ def test_different_axis(self): class Test__copy(tests.IrisTest): def test_basic(self): - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() meshcoord2 = meshcoord.copy() self.assertIsNot(meshcoord2, meshcoord) self.assertEqual(meshcoord2, meshcoord) @@ -295,12 +220,12 @@ def test_basic(self): self.assertIs(meshcoord2.mesh, meshcoord.mesh) def test_fail_copy_newpoints(self): - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() with self.assertRaisesRegex(ValueError, "Cannot change the content"): meshcoord.copy(points=meshcoord.points) def test_fail_copy_newbounds(self): - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() with self.assertRaisesRegex(ValueError, "Cannot change the content"): meshcoord.copy(bounds=meshcoord.bounds) @@ -308,7 +233,7 @@ def test_fail_copy_newbounds(self): class Test__getitem__(tests.IrisTest): def test_slice_wholeslice_1tuple(self): # The only slicing case that we support, to enable cube slicing. - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() meshcoord2 = meshcoord[ :, ] @@ -319,23 +244,23 @@ def test_slice_wholeslice_1tuple(self): def test_slice_whole_slice_singlekey(self): # A slice(None) also fails, if not presented in a 1-tuple. - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() with self.assertRaisesRegex(ValueError, "Cannot index"): meshcoord[:] def test_fail_slice_part(self): - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() with self.assertRaisesRegex(ValueError, "Cannot index"): meshcoord[:1] class Test__str_repr(tests.IrisTest): def setUp(self): - mesh = _create_test_mesh() + mesh = sample_mesh() self.mesh = mesh # Give mesh itself a name: makes a difference between str and repr. self.mesh.rename("test_mesh") - self.meshcoord = _create_test_meshcoord(mesh=mesh) + self.meshcoord = sample_meshcoord(mesh=mesh) def _expected_elements_regexp( self, @@ -371,9 +296,7 @@ def test__str__(self): self.assertRegex(result, re_expected) def test_alternative_location_and_axis(self): - meshcoord = _create_test_meshcoord( - mesh=self.mesh, location="edge", axis="y" - ) + meshcoord = sample_meshcoord(mesh=self.mesh, location="edge", axis="y") result = str(meshcoord) re_expected = r", location='edge', axis='y'" self.assertRegex(result, re_expected) @@ -384,7 +307,7 @@ def test_str_no_long_name(self): node_coord = mesh.coord(include_nodes=True, axis="x") node_coord.long_name = None # Make a new meshcoord, based on the modified mesh. - meshcoord = _create_test_meshcoord(mesh=self.mesh) + meshcoord = sample_meshcoord(mesh=self.mesh) result = str(meshcoord) re_expected = self._expected_elements_regexp(long_name=False) self.assertRegex(result, re_expected) @@ -396,7 +319,7 @@ def test_str_no_standard_name(self): node_coord.standard_name = None node_coord.axis = "x" # This is required : but it's a kludge !! # Make a new meshcoord, based on the modified mesh. - meshcoord = _create_test_meshcoord(mesh=self.mesh) + meshcoord = sample_meshcoord(mesh=self.mesh) result = str(meshcoord) re_expected = self._expected_elements_regexp(standard_name=False) self.assertRegex(result, re_expected) @@ -407,7 +330,7 @@ def test_str_no_attributes(self): node_coord = mesh.coord(include_nodes=True, axis="x") node_coord.attributes = None # Make a new meshcoord, based on the modified mesh. - meshcoord = _create_test_meshcoord(mesh=self.mesh) + meshcoord = sample_meshcoord(mesh=self.mesh) result = str(meshcoord) re_expected = self._expected_elements_regexp(attributes=False) self.assertRegex(result, re_expected) @@ -418,7 +341,7 @@ def test_str_empty_attributes(self): node_coord = mesh.coord(include_nodes=True, axis="x") node_coord.attributes.clear() # Make a new meshcoord, based on the modified mesh. - meshcoord = _create_test_meshcoord(mesh=self.mesh) + meshcoord = sample_meshcoord(mesh=self.mesh) result = str(meshcoord) re_expected = self._expected_elements_regexp(attributes=False) self.assertRegex(result, re_expected) @@ -428,8 +351,8 @@ class Test_cube_containment(tests.IrisTest): # Check that we can put a MeshCoord into a cube, and have it behave just # like a regular AuxCoord. def setUp(self): - meshcoord = _create_test_meshcoord() - data_shape = (2,) + _TEST_POINTS.shape + meshcoord = sample_meshcoord() + data_shape = (2,) + meshcoord.shape cube = Cube(np.zeros(data_shape)) cube.add_aux_coord(meshcoord, 1) self.meshcoord = meshcoord @@ -509,7 +432,7 @@ def test_cube_mesh_partslice(self): class Test_auxcoord_conversion(tests.IrisTest): def test_basic(self): - meshcoord = _create_test_meshcoord() + meshcoord = sample_meshcoord() auxcoord = AuxCoord.from_coord(meshcoord) for propname, auxval in auxcoord.metadata._asdict().items(): meshval = getattr(meshcoord, propname) From 92db9f6af44459de9974fe876fa69398f50a1eb7 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Wed, 17 Mar 2021 11:08:29 +0000 Subject: [PATCH 7/7] Make cube.mesh and cube.location properties. --- lib/iris/cube.py | 15 +++++++++------ lib/iris/tests/unit/cube/test_Cube.py | 16 ++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index afcdfb8888..f74bd9bc80 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1134,7 +1134,7 @@ def _check_multi_dim_metadata(self, metadata, data_dims): def _add_unique_aux_coord(self, coord, data_dims): data_dims = self._check_multi_dim_metadata(coord, data_dims) if hasattr(coord, "mesh"): - mesh = self.mesh() + mesh = self.mesh if mesh: msg = ( "{item} of Meshcoord {coord!r} is " @@ -1150,7 +1150,7 @@ def _add_unique_aux_coord(self, coord, data_dims): ownval=mesh, ) ) - location = self.location() + location = self.location if coord.location != location: raise ValueError( msg.format( @@ -1957,7 +1957,8 @@ def coord_system(self, spec=None): return result - def _a_meshcoord(self): + def _any_meshcoord(self): + """Return a MeshCoord if there are any, else None.""" mesh_coords = self.coords(mesh_coords=True) if mesh_coords: result = mesh_coords[0] @@ -1965,6 +1966,7 @@ def _a_meshcoord(self): result = None return result + @property def mesh(self): """ Return the unstructured :class:`~iris.experimental.ugrid.Mesh` @@ -1980,11 +1982,12 @@ def mesh(self): or ``None``. """ - result = self._a_meshcoord() + result = self._any_meshcoord() if result is not None: result = result.mesh return result + @property def location(self): """ Return the mesh "location" of the cube data, if the cube has any @@ -2000,7 +2003,7 @@ def location(self): or ``None``. """ - result = self._a_meshcoord() + result = self._any_meshcoord() if result is not None: result = result.location return result @@ -2019,7 +2022,7 @@ def mesh_dim(self): or ``None``. """ - result = self._a_meshcoord() + result = self._any_meshcoord() if result is not None: (result,) = self.coord_dims(result) # result is a 1-tuple return result diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index e273e2edde..bbdecd7ec9 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2051,13 +2051,13 @@ def setUp(self): _add_test_meshcube(self) def test_mesh(self): - result = self.cube.mesh() + result = self.cube.mesh self.assertIs(result, self.mesh) def test_no_mesh(self): # Replace standard setUp cube with a no-mesh version. _add_test_meshcube(self, nomesh=True) - result = self.cube.mesh() + result = self.cube.mesh self.assertIsNone(result) @@ -2069,19 +2069,19 @@ def setUp(self): def test_no_mesh(self): # Replace standard setUp cube with a no-mesh version. _add_test_meshcube(self, nomesh=True) - result = self.cube.location() + result = self.cube.location self.assertIsNone(result) def test_mesh(self): cube = self.cube - result = cube.location() + result = cube.location self.assertEqual(result, self.meshco_x.location) def test_alternate_location(self): # Replace standard setUp cube with an edge-based version. _add_test_meshcube(self, location="edge") cube = self.cube - result = cube.location() + result = cube.location self.assertEqual(result, "edge") @@ -2138,7 +2138,7 @@ def test_mesh(self): dim_coords_and_dims=[(dimco_z, 0), (dimco_mesh, 1)], aux_coords_and_dims=[(meshco, 1)], ) - self.assertEqual(cube.mesh(), meshco.mesh) + self.assertEqual(cube.mesh, meshco.mesh) def test_fail_dim_meshcoord(self): # As "test_mesh", but attempt to use the meshcoord as a dim-coord. @@ -2160,7 +2160,7 @@ def test_multi_meshcoords(self): np.zeros(n_faces), aux_coords_and_dims=[(meshco_x, 0), (meshco_y, 0)], ) - self.assertEqual(cube.mesh(), meshco_x.mesh) + self.assertEqual(cube.mesh, meshco_x.mesh) def test_multi_meshcoords_same_axis(self): # *Not* an error, as long as the coords are distinguishable. @@ -2175,7 +2175,7 @@ def test_multi_meshcoords_same_axis(self): np.zeros(n_faces), aux_coords_and_dims=[(meshco_1, 0), (meshco_2, 0)], ) - self.assertEqual(cube.mesh(), meshco_1.mesh) + self.assertEqual(cube.mesh, meshco_1.mesh) def test_fail_meshcoords_different_locations(self): # Same as successful 'multi_mesh', but different locations.