diff --git a/benchmarks/benchmarks/cperf/__init__.py b/benchmarks/benchmarks/cperf/__init__.py index df28a66265..05a086bc44 100644 --- a/benchmarks/benchmarks/cperf/__init__.py +++ b/benchmarks/benchmarks/cperf/__init__.py @@ -14,9 +14,6 @@ from iris import load_cube -# TODO: remove uses of PARSE_UGRID_ON_LOAD once UGRID parsing is core behaviour. -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - from ..generate_data import BENCHMARK_DATA from ..generate_data.ugrid import make_cubesphere_testfile @@ -92,5 +89,4 @@ def setup(self, file_type, three_d, three_times): self.file_type = file_type def load(self): - with PARSE_UGRID_ON_LOAD.context(): - return load_cube(str(self.file_path)) + return load_cube(str(self.file_path)) diff --git a/benchmarks/benchmarks/generate_data/stock.py b/benchmarks/benchmarks/generate_data/stock.py index 61f085195a..f16114f183 100644 --- a/benchmarks/benchmarks/generate_data/stock.py +++ b/benchmarks/benchmarks/generate_data/stock.py @@ -14,7 +14,7 @@ import iris from iris import cube -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD, load_mesh +from iris.experimental.ugrid import load_mesh from . import BENCHMARK_DATA, REUSE_DATA, load_realised, run_function_elsewhere @@ -104,13 +104,12 @@ def _external(*args, **kwargs): save_path = (BENCHMARK_DATA / f"sample_mesh_{args_hash}").with_suffix(".nc") if not REUSE_DATA or not save_path.is_file(): _ = run_function_elsewhere(_external, *arg_list, save_path=str(save_path)) - with PARSE_UGRID_ON_LOAD.context(): - if not lazy_values: - # Realise everything. - with load_realised(): - mesh = load_mesh(str(save_path)) - else: + if not lazy_values: + # Realise everything. + with load_realised(): mesh = load_mesh(str(save_path)) + else: + mesh = load_mesh(str(save_path)) return mesh @@ -118,7 +117,7 @@ def sample_meshcoord(sample_mesh_kwargs=None, location="face", axis="x"): """Sample meshcoord wrapper for :meth:`iris.tests.stock.mesh.sample_meshcoord`. Parameters deviate from the original as cannot pass a - :class:`iris.experimental.ugrid.Mesh to the separate Python instance - must + :class:`iris.mesh.Mesh to the separate Python instance - must instead generate the Mesh as well. MeshCoords cannot be saved to file, so the _external method saves the @@ -147,9 +146,8 @@ def _external(sample_mesh_kwargs_, save_path_): sample_mesh_kwargs_=sample_mesh_kwargs, save_path_=str(save_path), ) - with PARSE_UGRID_ON_LOAD.context(): - with load_realised(): - source_mesh = load_mesh(str(save_path)) + with load_realised(): + source_mesh = load_mesh(str(save_path)) # Regenerate MeshCoord from its Mesh, which we saved. return source_mesh.to_MeshCoord(location=location, axis=axis) @@ -180,7 +178,6 @@ def _external(w_mesh_: str, save_path_: str): ) if not REUSE_DATA or not save_path.is_file(): _ = run_function_elsewhere(_external, w_mesh_=w_mesh, save_path_=str(save_path)) - with PARSE_UGRID_ON_LOAD.context(): - context = nullcontext() if lazy else load_realised() - with context: - return iris.load_cube(save_path, "air_potential_temperature") + context = nullcontext() if lazy else load_realised() + with context: + return iris.load_cube(save_path, "air_potential_temperature") diff --git a/benchmarks/benchmarks/generate_data/ugrid.py b/benchmarks/benchmarks/generate_data/ugrid.py index de76d63798..2cef4752ee 100644 --- a/benchmarks/benchmarks/generate_data/ugrid.py +++ b/benchmarks/benchmarks/generate_data/ugrid.py @@ -5,7 +5,6 @@ """Scripts for generating supporting data for UGRID-related benchmarking.""" from iris import load_cube as iris_loadcube -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD from . import BENCHMARK_DATA, REUSE_DATA, load_realised, run_function_elsewhere from .stock import ( @@ -85,8 +84,7 @@ def make_cube_like_2d_cubesphere(n_cube: int, with_mesh: bool): ) # File now *should* definitely exist: content is simply the desired cube. - with PARSE_UGRID_ON_LOAD.context(): - cube = iris_loadcube(str(filepath)) + cube = iris_loadcube(str(filepath)) # Ensure correct laziness. _ = cube.data @@ -155,9 +153,8 @@ def _external(xy_dims_, save_path_): ) if not REUSE_DATA or not save_path.is_file(): _ = run_function_elsewhere(_external, xy_dims, str(save_path)) - with PARSE_UGRID_ON_LOAD.context(): - with load_realised(): - cube = iris_loadcube(str(save_path)) + with load_realised(): + cube = iris_loadcube(str(save_path)) return cube diff --git a/benchmarks/benchmarks/load/ugrid.py b/benchmarks/benchmarks/load/ugrid.py index 47e23dc050..093cf6950a 100644 --- a/benchmarks/benchmarks/load/ugrid.py +++ b/benchmarks/benchmarks/load/ugrid.py @@ -5,7 +5,6 @@ """Mesh data loading benchmark tests.""" from iris import load_cube as iris_load_cube -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD from iris.experimental.ugrid import load_mesh as iris_load_mesh from ..generate_data.stock import create_file__xios_2d_face_half_levels @@ -18,13 +17,11 @@ def synthetic_data(**kwargs): def load_cube(*args, **kwargs): - with PARSE_UGRID_ON_LOAD.context(): - return iris_load_cube(*args, **kwargs) + return iris_load_cube(*args, **kwargs) def load_mesh(*args, **kwargs): - with PARSE_UGRID_ON_LOAD.context(): - return iris_load_mesh(*args, **kwargs) + return iris_load_mesh(*args, **kwargs) class BasicLoading: diff --git a/benchmarks/benchmarks/experimental/__init__.py b/benchmarks/benchmarks/mesh/__init__.py similarity index 77% rename from benchmarks/benchmarks/experimental/__init__.py rename to benchmarks/benchmarks/mesh/__init__.py index ce727a7286..9cc76ce0aa 100644 --- a/benchmarks/benchmarks/experimental/__init__.py +++ b/benchmarks/benchmarks/mesh/__init__.py @@ -2,4 +2,4 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Benchmark tests for the experimental module.""" +"""Benchmark tests for the iris.mesh module.""" diff --git a/benchmarks/benchmarks/experimental/ugrid/__init__.py b/benchmarks/benchmarks/mesh/utils/__init__.py similarity index 75% rename from benchmarks/benchmarks/experimental/ugrid/__init__.py rename to benchmarks/benchmarks/mesh/utils/__init__.py index 4976054178..e20973c0a7 100644 --- a/benchmarks/benchmarks/experimental/ugrid/__init__.py +++ b/benchmarks/benchmarks/mesh/utils/__init__.py @@ -2,4 +2,4 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Benchmark tests for the experimental.ugrid module.""" +"""Benchmark tests for the iris.mesh.utils module.""" diff --git a/benchmarks/benchmarks/experimental/ugrid/regions_combine.py b/benchmarks/benchmarks/mesh/utils/regions_combine.py similarity index 94% rename from benchmarks/benchmarks/experimental/ugrid/regions_combine.py rename to benchmarks/benchmarks/mesh/utils/regions_combine.py index d3781a183f..9d97eb07dd 100644 --- a/benchmarks/benchmarks/experimental/ugrid/regions_combine.py +++ b/benchmarks/benchmarks/mesh/utils/regions_combine.py @@ -5,7 +5,7 @@ """Benchmarks stages of operation. Benchmarks stages of operation of the function -:func:`iris.experimental.ugrid.utils.recombine_submeshes`. +:func:`iris.mesh.utils.recombine_submeshes`. """ @@ -15,8 +15,7 @@ import numpy as np from iris import load, load_cube, save -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD -from iris.experimental.ugrid.utils import recombine_submeshes +from iris.experimental.ugrid import recombine_submeshes from ... import TrackAddedMemoryAllocation from ...generate_data.ugrid import make_cube_like_2d_cubesphere @@ -103,13 +102,12 @@ def setup(self, n_cubesphere, imaginary_data=True, create_result_cube=True): """ # Load source cubes (full-mesh and regions) - with PARSE_UGRID_ON_LOAD.context(): - self.full_mesh_cube = load_cube( - self._parametrised_cache_filename(n_cubesphere, "meshcube") - ) - self.region_cubes = load( - self._parametrised_cache_filename(n_cubesphere, "regioncubes") - ) + self.full_mesh_cube = load_cube( + self._parametrised_cache_filename(n_cubesphere, "meshcube") + ) + self.region_cubes = load( + self._parametrised_cache_filename(n_cubesphere, "regioncubes") + ) # Remove all var-names from loaded cubes, which can otherwise cause # problems. Also implement 'imaginary' data. diff --git a/benchmarks/benchmarks/sperf/__init__.py b/benchmarks/benchmarks/sperf/__init__.py index e51bef5ca2..2b8b508fd5 100644 --- a/benchmarks/benchmarks/sperf/__init__.py +++ b/benchmarks/benchmarks/sperf/__init__.py @@ -10,9 +10,6 @@ from iris import load_cube -# TODO: remove uses of PARSE_UGRID_ON_LOAD once UGRID parsing is core behaviour. -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - from ..generate_data.ugrid import make_cubesphere_testfile @@ -38,5 +35,4 @@ def setup(self, c_size, n_levels, n_times): ) def load_cube(self): - with PARSE_UGRID_ON_LOAD.context(): - return load_cube(str(self.file_path)) + return load_cube(str(self.file_path)) diff --git a/benchmarks/benchmarks/sperf/combine_regions.py b/benchmarks/benchmarks/sperf/combine_regions.py index d375f44719..714d7f9624 100644 --- a/benchmarks/benchmarks/sperf/combine_regions.py +++ b/benchmarks/benchmarks/sperf/combine_regions.py @@ -10,8 +10,7 @@ import numpy as np from iris import load, load_cube, save -from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD -from iris.experimental.ugrid.utils import recombine_submeshes +from iris.experimental.ugrid import recombine_submeshes from .. import TrackAddedMemoryAllocation, on_demand_benchmark from ..generate_data.ugrid import BENCHMARK_DATA, make_cube_like_2d_cubesphere @@ -102,13 +101,12 @@ def setup(self, n_cubesphere, imaginary_data=True, create_result_cube=True): """ # Load source cubes (full-mesh and regions) - with PARSE_UGRID_ON_LOAD.context(): - self.full_mesh_cube = load_cube( - self._parametrised_cache_filename(n_cubesphere, "meshcube") - ) - self.region_cubes = load( - self._parametrised_cache_filename(n_cubesphere, "regioncubes") - ) + self.full_mesh_cube = load_cube( + self._parametrised_cache_filename(n_cubesphere, "meshcube") + ) + self.region_cubes = load( + self._parametrised_cache_filename(n_cubesphere, "regioncubes") + ) # Remove all var-names from loaded cubes, which can otherwise cause # problems. Also implement 'imaginary' data. diff --git a/benchmarks/benchmarks/sperf/equality.py b/benchmarks/benchmarks/sperf/equality.py index f67935c9ef..ddee90cd28 100644 --- a/benchmarks/benchmarks/sperf/equality.py +++ b/benchmarks/benchmarks/sperf/equality.py @@ -13,7 +13,7 @@ class CubeEquality(FileMixin): r"""Benchmark time and memory costs. Benchmark time and memory costs of comparing :class:`~iris.cube.Cube`\\ s - with attached :class:`~iris.experimental.ugrid.mesh.MeshXY`\\ es. + with attached :class:`~iris.mesh.MeshXY`\\ es. Uses :class:`FileMixin` as the realistic case will be comparing :class:`~iris.cube.Cube`\\ s that have been loaded from file. diff --git a/benchmarks/benchmarks/unit_style/ugrid.py b/benchmarks/benchmarks/unit_style/mesh.py similarity index 93% rename from benchmarks/benchmarks/unit_style/ugrid.py rename to benchmarks/benchmarks/unit_style/mesh.py index e2f235eb28..ed3aad1428 100644 --- a/benchmarks/benchmarks/unit_style/ugrid.py +++ b/benchmarks/benchmarks/unit_style/mesh.py @@ -2,22 +2,22 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Benchmark tests for the experimental.ugrid module.""" +"""Benchmark tests for the iris.mesh module.""" from copy import deepcopy import numpy as np -from iris.experimental import ugrid +from iris import mesh from .. import disable_repeat_between_setup from ..generate_data.stock import sample_mesh class UGridCommon: - """Run a generalised suite of benchmarks for any ugrid object. + """Run a generalised suite of benchmarks for any mesh object. - A base class running a generalised suite of benchmarks for any ugrid object. + A base class running a generalised suite of benchmarks for any mesh object. Object to be specified in a subclass. ASV will run the benchmarks within this class for any subclasses. @@ -53,7 +53,7 @@ def setup(self, n_faces): super().setup(n_faces) def create(self): - return ugrid.Connectivity(indices=self.array, cf_role="face_node_connectivity") + return mesh.Connectivity(indices=self.array, cf_role="face_node_connectivity") def time_indices(self, n_faces): _ = self.object.indices @@ -123,7 +123,7 @@ def get_coords_and_axes(location): self.eq_object = deepcopy(self.object) def create(self): - return ugrid.MeshXY(**self.mesh_kwargs) + return mesh.MeshXY(**self.mesh_kwargs) def time_add_connectivities(self, n_faces): self.object.add_connectivities(self.face_node) @@ -170,7 +170,7 @@ def setup(self, n_faces, lazy=False): super().setup(n_faces) def create(self): - return ugrid.MeshCoord(mesh=self.mesh, location="face", axis="x") + return mesh.MeshCoord(mesh=self.mesh, location="face", axis="x") def time_points(self, n_faces): _ = self.object.points diff --git a/docs/src/further_topics/ugrid/data_model.rst b/docs/src/further_topics/ugrid/data_model.rst index d7282c71d8..1660f6d08c 100644 --- a/docs/src/further_topics/ugrid/data_model.rst +++ b/docs/src/further_topics/ugrid/data_model.rst @@ -298,7 +298,7 @@ How Iris Represents This .. seealso:: Remember this is a prose summary. Precise documentation is at: - :mod:`iris.experimental.ugrid`. + :mod:`iris.mesh`. .. note:: @@ -310,7 +310,7 @@ The Basics The Iris :class:`~iris.cube.Cube` has several new members: * | :attr:`~iris.cube.Cube.mesh` - | The :class:`iris.experimental.ugrid.MeshXY` that describes the + | The :class:`iris.mesh.MeshXY` that describes the :class:`~iris.cube.Cube`\'s horizontal geography. * | :attr:`~iris.cube.Cube.location` | ``node``/``edge``/``face`` - the mesh element type with which this @@ -320,10 +320,10 @@ The Iris :class:`~iris.cube.Cube` has several new members: indexes over the horizontal :attr:`~iris.cube.Cube.data` positions. These members will all be ``None`` for a :class:`~iris.cube.Cube` with no -associated :class:`~iris.experimental.ugrid.MeshXY`. +associated :class:`~iris.mesh.MeshXY`. This :class:`~iris.cube.Cube`\'s unstructured dimension has multiple attached -:class:`iris.experimental.ugrid.MeshCoord`\s (one for each axis e.g. +:class:`iris.mesh.MeshCoord`\s (one for each axis e.g. ``x``/``y``), which can be used to infer the points and bounds of any index on the :class:`~iris.cube.Cube`\'s unstructured dimension. @@ -333,7 +333,7 @@ the :class:`~iris.cube.Cube`\'s unstructured dimension. from iris.coords import AuxCoord, DimCoord from iris.cube import Cube - from iris.experimental.ugrid import Connectivity, MeshXY + from iris.mesh import Connectivity, MeshXY node_x = AuxCoord( points=[0.0, 5.0, 0.0, 5.0, 8.0], @@ -422,38 +422,38 @@ The Detail ---------- How UGRID information is stored ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* | :class:`iris.experimental.ugrid.MeshXY` +* | :class:`iris.mesh.MeshXY` | Contains all information about the mesh. | Includes: - * | :attr:`~iris.experimental.ugrid.MeshXY.topology_dimension` + * | :attr:`~iris.mesh.MeshXY.topology_dimension` | The maximum dimensionality of shape (1D=edge, 2D=face) supported - by this :class:`~iris.experimental.ugrid.MeshXY`. Determines which - :class:`~iris.experimental.ugrid.Connectivity`\s are required/optional + by this :class:`~iris.mesh.MeshXY`. Determines which + :class:`~iris.mesh.Connectivity`\s are required/optional (see below). * 1-3 collections of :class:`iris.coords.AuxCoord`\s: - * | **Required**: :attr:`~iris.experimental.ugrid.MeshXY.node_coords` + * | **Required**: :attr:`~iris.mesh.MeshXY.node_coords` | The nodes that are the basis for the mesh. - * | Optional: :attr:`~iris.experimental.ugrid.MeshXY.edge_coords`, - :attr:`~iris.experimental.ugrid.MeshXY.face_coords` + * | Optional: :attr:`~iris.mesh.Mesh.edge_coords`, + :attr:`~iris.mesh.MeshXY.face_coords` | For indicating the 'centres' of the edges/faces. - | **NOTE:** generating a :class:`~iris.experimental.ugrid.MeshCoord` from - a :class:`~iris.experimental.ugrid.MeshXY` currently (``Jan 2022``) + | **NOTE:** generating a :class:`~iris.mesh.MeshCoord` from + a :class:`~iris.mesh.MeshXY` currently (``Jan 2022``) requires centre coordinates for the given ``location``; to be rectified in future. - * 1 or more :class:`iris.experimental.ugrid.Connectivity`\s: + * 1 or more :class:`iris.mesh.Connectivity`\s: * | **Required for 1D (edge) elements**: - :attr:`~iris.experimental.ugrid.MeshXY.edge_node_connectivity` + :attr:`~iris.mesh.MeshXY.edge_node_connectivity` | Define the edges by connecting nodes. * | **Required for 2D (face) elements**: - :attr:`~iris.experimental.ugrid.MeshXY.face_node_connectivity` + :attr:`~iris.mesh.MeshXY.face_node_connectivity` | Define the faces by connecting nodes. * Optional: any other connectivity type. See - :attr:`iris.experimental.ugrid.mesh.Connectivity.UGRID_CF_ROLES` for the + :attr:`iris.mesh.Connectivity.UGRID_CF_ROLES` for the full list of types. .. doctest:: ugrid_summaries @@ -480,30 +480,30 @@ How UGRID information is stored long_name: 'my_mesh' -* | :class:`iris.experimental.ugrid.MeshCoord` +* | :class:`iris.mesh.MeshCoord` | Described in detail in `MeshCoords`_. | Stores the following information: - * | :attr:`~iris.experimental.ugrid.MeshCoord.mesh` - | The :class:`~iris.experimental.ugrid.MeshXY` associated with this - :class:`~iris.experimental.ugrid.MeshCoord`. This determines the + * | :attr:`~iris.mesh.MeshCoord.mesh` + | The :class:`~iris.mesh.MeshXY` associated with this + :class:`~iris.mesh.MeshCoord`. This determines the :attr:`~iris.cube.Cube.mesh` attribute of any :class:`~iris.cube.Cube` - this :class:`~iris.experimental.ugrid.MeshCoord` is attached to (see + this :class:`~iris.mesh.MeshCoord` is attached to (see `The Basics`_) - * | :attr:`~iris.experimental.ugrid.MeshCoord.location` + * | :attr:`~iris.mesh.MeshCoord.location` | ``node``/``edge``/``face`` - the element detailed by this - :class:`~iris.experimental.ugrid.MeshCoord`. This determines the + :class:`~iris.mesh.MeshCoord`. This determines the :attr:`~iris.cube.Cube.location` attribute of any :class:`~iris.cube.Cube` this - :class:`~iris.experimental.ugrid.MeshCoord` is attached to (see + :class:`~iris.mesh.MeshCoord` is attached to (see `The Basics`_). .. _ugrid MeshCoords: MeshCoords ~~~~~~~~~~ -Links a :class:`~iris.cube.Cube` to a :class:`~iris.experimental.ugrid.MeshXY` by +Links a :class:`~iris.cube.Cube` to a :class:`~iris.mesh.MeshXY` by attaching to the :class:`~iris.cube.Cube`\'s unstructured dimension, in the same way that all :class:`~iris.coords.Coord`\s attach to :class:`~iris.cube.Cube` dimensions. This allows a single @@ -511,23 +511,23 @@ same way that all :class:`~iris.coords.Coord`\s attach to dimensions (e.g. horizontal mesh plus vertical levels and a time series), using the same logic for every dimension. -:class:`~iris.experimental.ugrid.MeshCoord`\s are instantiated using a given -:class:`~iris.experimental.ugrid.MeshXY`, ``location`` +:class:`~iris.mesh.MeshCoord`\s are instantiated using a given +:class:`~iris.mesh.MeshXY`, ``location`` ("node"/"edge"/"face") and ``axis``. The process interprets the -:class:`~iris.experimental.ugrid.MeshXY`\'s -:attr:`~iris.experimental.ugrid.MeshXY.node_coords` and if appropriate the -:attr:`~iris.experimental.ugrid.MeshXY.edge_node_connectivity`/ -:attr:`~iris.experimental.ugrid.MeshXY.face_node_connectivity` and -:attr:`~iris.experimental.ugrid.MeshXY.edge_coords`/ -:attr:`~iris.experimental.ugrid.MeshXY.face_coords` +:class:`~iris.mesh.MeshXY`\'s +:attr:`~iris.mesh.MeshXY.node_coords` and if appropriate the +:attr:`~iris.mesh.MeshXY.edge_node_connectivity`/ +:attr:`~iris.mesh.MeshXY.face_node_connectivity` and +:attr:`~iris.mesh.MeshXY.edge_coords`/ +:attr:`~iris.mesh.MeshXY.face_coords` to produce a :class:`~iris.coords.Coord` :attr:`~iris.coords.Coord.points` and :attr:`~iris.coords.Coord.bounds` -representation of all the :class:`~iris.experimental.ugrid.MeshXY`\'s +representation of all the :class:`~iris.mesh.MeshXY`\'s nodes/edges/faces for the given axis. -The method :meth:`iris.experimental.ugrid.MeshXY.to_MeshCoords` is available to -create a :class:`~iris.experimental.ugrid.MeshCoord` for -every axis represented by that :class:`~iris.experimental.ugrid.MeshXY`, +The method :meth:`iris.mesh.MeshXY.to_MeshCoords` is available to +create a :class:`~iris.mesh.MeshCoord` for +every axis represented by that :class:`~iris.mesh.MeshXY`, given only the ``location`` argument .. doctest:: ugrid_summaries diff --git a/docs/src/further_topics/ugrid/index.rst b/docs/src/further_topics/ugrid/index.rst index c45fd271a2..c247a9dc6d 100644 --- a/docs/src/further_topics/ugrid/index.rst +++ b/docs/src/further_topics/ugrid/index.rst @@ -9,7 +9,7 @@ Iris includes specialised handling of mesh-located data (as opposed to grid-located data). Iris and its :ref:`partner packages ` are designed to make working with mesh-located data as simple as possible, with new capabilities being added all the time. More detail is in this section and in -the :mod:`iris.experimental.ugrid` API documentation. +the :mod:`iris.mesh` API documentation. This mesh support is based on the `CF-UGRID Conventions`__; UGRID-conformant meshes + data can be loaded from a file into Iris' data model, and meshes + diff --git a/docs/src/further_topics/ugrid/operations.rst b/docs/src/further_topics/ugrid/operations.rst index 80ff284f66..97dfaaa5b1 100644 --- a/docs/src/further_topics/ugrid/operations.rst +++ b/docs/src/further_topics/ugrid/operations.rst @@ -61,7 +61,7 @@ subsequent example operations on this page. >>> import numpy as np >>> from iris.coords import AuxCoord - >>> from iris.experimental.ugrid import Connectivity, MeshXY + >>> from iris.mesh import Connectivity, MeshXY # Going to create the following mesh # (node indices are shown to aid understanding): @@ -143,8 +143,8 @@ Making a Cube (with a Mesh) .. rubric:: |tagline: making a cube| Creating a :class:`~iris.cube.Cube` is unchanged; the -:class:`~iris.experimental.ugrid.MeshXY` is linked via a -:class:`~iris.experimental.ugrid.MeshCoord` (see :ref:`ugrid MeshCoords`): +:class:`~iris.mesh.MeshXY` is linked via a +:class:`~iris.mesh.MeshCoord` (see :ref:`ugrid MeshCoords`): .. dropdown:: Code :icon: code @@ -205,7 +205,7 @@ Save .. note:: UGRID saving support is limited to the NetCDF file format. The Iris saving process automatically detects if the :class:`~iris.cube.Cube` -has an associated :class:`~iris.experimental.ugrid.MeshXY` and automatically +has an associated :class:`~iris.mesh.MeshXY` and automatically saves the file in a UGRID-conformant format: .. dropdown:: Code @@ -282,8 +282,8 @@ saves the file in a UGRID-conformant format: } -The :func:`iris.experimental.ugrid.save_mesh` function allows -:class:`~iris.experimental.ugrid.MeshXY`\es to be saved to file without +The :func:`iris.mesh.save_mesh` function allows +:class:`~iris.mesh.MeshXY`\es to be saved to file without associated :class:`~iris.cube.Cube`\s: .. dropdown:: Code @@ -293,7 +293,7 @@ associated :class:`~iris.cube.Cube`\s: >>> from subprocess import run - >>> from iris.experimental.ugrid import save_mesh + >>> from iris.mesh import save_mesh >>> mesh_path = "my_mesh.nc" >>> save_mesh(my_mesh, mesh_path) @@ -347,16 +347,14 @@ associated :class:`~iris.cube.Cube`\s: Load ---- -.. |tagline: load| replace:: |different| - UGRID parsing is opt-in +.. |tagline: load| replace:: |unchanged| .. rubric:: |tagline: load| .. note:: UGRID loading support is limited to the NetCDF file format. -While Iris' UGRID support remains :mod:`~iris.experimental`, parsing UGRID when -loading a file remains **optional**. To load UGRID data from a file into the -Iris mesh data model, use the -:const:`iris.experimental.ugrid.PARSE_UGRID_ON_LOAD` context manager: +Iris mesh support detects + parses any UGRID information when loading files, to +produce cubes with a non-empty ".mesh" property. .. dropdown:: Code :icon: code @@ -364,10 +362,8 @@ Iris mesh data model, use the .. doctest:: ugrid_operations >>> from iris import load - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - >>> with PARSE_UGRID_ON_LOAD.context(): - ... loaded_cubelist = load(cubelist_path) + >>> loaded_cubelist = load(cubelist_path) # Sort CubeList to ensure consistent result. >>> loaded_cubelist.sort(key=lambda cube: cube.name()) @@ -386,9 +382,8 @@ etcetera: >>> from iris import Constraint, load_cube - >>> with PARSE_UGRID_ON_LOAD.context(): - ... ground_cubelist = load(cubelist_path, Constraint(height=0)) - ... face_cube = load_cube(cubelist_path, "face_data") + >>> ground_cubelist = load(cubelist_path, Constraint(height=0)) + >>> face_cube = load_cube(cubelist_path, "face_data") # Sort CubeList to ensure consistent result. >>> ground_cubelist.sort(key=lambda cube: cube.name()) @@ -412,15 +407,15 @@ etcetera: .. note:: We recommend caution if constraining on coordinates associated with a - :class:`~iris.experimental.ugrid.MeshXY`. An individual coordinate value + :class:`~iris.mesh.MeshXY`. An individual coordinate value might not be shared by any other data points, and using a coordinate range will demand notably higher performance given the size of the dimension versus structured grids (:ref:`see the data model detail `). -The :func:`iris.experimental.ugrid.load_mesh` and -:func:`~iris.experimental.ugrid.load_meshes` functions allow only -:class:`~iris.experimental.ugrid.MeshXY`\es to be loaded from a file without +The :func:`iris.mesh.load_mesh` and +:func:`~iris.mesh.load_meshes` functions allow only +:class:`~iris.mesh.MeshXY`\es to be loaded from a file without creating any associated :class:`~iris.cube.Cube`\s: .. dropdown:: Code @@ -428,10 +423,9 @@ creating any associated :class:`~iris.cube.Cube`\s: .. doctest:: ugrid_operations - >>> from iris.experimental.ugrid import load_mesh + >>> from iris.mesh import load_mesh - >>> with PARSE_UGRID_ON_LOAD.context(): - ... loaded_mesh = load_mesh(cubelist_path) + >>> loaded_mesh = load_mesh(cubelist_path) >>> print(loaded_mesh) MeshXY : 'my_mesh' @@ -493,10 +487,8 @@ GeoVista :external+geovista:doc:`generated/gallery/index`. >>> from iris import load_cube, sample_data_path >>> from iris.experimental.geovista import cube_to_polydata - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - >>> with PARSE_UGRID_ON_LOAD.context(): - ... sample_mesh_cube = load_cube(sample_data_path("mesh_C4_synthetic_float.nc")) + >>> sample_mesh_cube = load_cube(sample_data_path("mesh_C4_synthetic_float.nc")) >>> print(sample_mesh_cube) synthetic / (1) (-- : 96) Mesh coordinates: @@ -541,11 +533,11 @@ As described in :doc:`data_model`, indexing for a range along a :class:`~iris.cube.Cube`\'s :meth:`~iris.cube.Cube.mesh_dim` will not provide a contiguous region, since **position on the unstructured dimension is unrelated to spatial position**. This means that subsetted -:class:`~iris.experimental.ugrid.MeshCoord`\s cannot be reliably interpreted -as intended, and subsetting a :class:`~iris.experimental.ugrid.MeshCoord` is +:class:`~iris.mesh.MeshCoord`\s cannot be reliably interpreted +as intended, and subsetting a :class:`~iris.mesh.MeshCoord` is therefore set to return an :class:`~iris.coords.AuxCoord` instead - breaking the link between :class:`~iris.cube.Cube` and -:class:`~iris.experimental.ugrid.MeshXY`: +:class:`~iris.mesh.MeshXY`: .. dropdown:: Code :icon: code @@ -595,10 +587,8 @@ below: >>> from geovista.geodesic import BBox >>> from iris import load_cube, sample_data_path >>> from iris.experimental.geovista import cube_to_polydata, extract_unstructured_region - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - >>> with PARSE_UGRID_ON_LOAD.context(): - ... sample_mesh_cube = load_cube(sample_data_path("mesh_C4_synthetic_float.nc")) + >>> sample_mesh_cube = load_cube(sample_data_path("mesh_C4_synthetic_float.nc")) >>> print(sample_mesh_cube) synthetic / (1) (-- : 96) Mesh coordinates: @@ -667,7 +657,6 @@ with the >>> from esmf_regrid.experimental.unstructured_scheme import MeshToGridESMFRegridder >>> from iris import load, load_cube - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD # You could also download these files from github.com/SciTools/iris-test-data. >>> from iris.tests import get_data_path @@ -679,8 +668,7 @@ with the ... ) # Load a list of cubes defined on the same Mesh. - >>> with PARSE_UGRID_ON_LOAD.context(): - ... mesh_cubes = load(mesh_file) + >>> mesh_cubes = load(mesh_file) # Extract a specific cube. >>> mesh_cube1 = mesh_cubes.extract_cube("sea_surface_temperature") @@ -751,7 +739,7 @@ with the The initialisation process is computationally expensive so we use caching to improve performance. Once a regridder has been initialised, it can be used on any :class:`~iris.cube.Cube` which has been defined on the same -:class:`~iris.experimental.ugrid.MeshXY` (or on the same **grid** in the case of +:class:`~iris.mesh.MeshXY` (or on the same **grid** in the case of :class:`~esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`). Since calling a regridder is usually a lot faster than initialising, reusing regridders can save a lot of time. We can demonstrate the reuse of the @@ -819,19 +807,19 @@ Equality .. rubric:: |tagline: equality| -:class:`~iris.experimental.ugrid.MeshXY` comparison is supported, and comparing -two ':class:`~iris.experimental.ugrid.MeshXY`-:class:`~iris.cube.Cube`\s' will +:class:`~iris.mesh.MeshXY` comparison is supported, and comparing +two ':class:`~iris.mesh.MeshXY`-:class:`~iris.cube.Cube`\s' will include a comparison of the respective -:class:`~iris.experimental.ugrid.MeshXY`\es, with no extra action needed by the +:class:`~iris.mesh.MeshXY`\es, with no extra action needed by the user. .. note:: Keep an eye on memory demand when comparing large - :class:`~iris.experimental.ugrid.MeshXY`\es, but note that - :class:`~iris.experimental.ugrid.MeshXY`\ equality is enabled for lazy + :class:`~iris.mesh.MeshXY`\es, but note that + :class:`~iris.mesh.MeshXY`\ equality is enabled for lazy processing (:doc:`/userguide/real_and_lazy_data`), so if the - :class:`~iris.experimental.ugrid.MeshXY`\es being compared are lazy the + :class:`~iris.mesh.MeshXY`\es being compared are lazy the process will use less memory than their total size. Combining Cubes @@ -842,23 +830,23 @@ Combining Cubes Merging or concatenating :class:`~iris.cube.Cube`\s (described in :doc:`/userguide/merge_and_concat`) with two different -:class:`~iris.experimental.ugrid.MeshXY`\es is not possible - a +:class:`~iris.mesh.MeshXY`\es is not possible - a :class:`~iris.cube.Cube` must be associated with just a single -:class:`~iris.experimental.ugrid.MeshXY`, and merge/concatenate are not yet -capable of combining multiple :class:`~iris.experimental.ugrid.MeshXY`\es into +:class:`~iris.mesh.MeshXY`, and merge/concatenate are not yet +capable of combining multiple :class:`~iris.mesh.MeshXY`\es into one. :class:`~iris.cube.Cube`\s that include -:class:`~iris.experimental.ugrid.MeshCoord`\s can still be merged/concatenated -on dimensions other than the :meth:`~iris.cube.Cube.mesh_dim`, since such -:class:`~iris.cube.Cube`\s will by definition share the same -:class:`~iris.experimental.ugrid.MeshXY`. +:class:`~iris.mesh.MeshCoord`\s can still be merged/concatenated +on dimensions other than the :meth:`~iris.cube.Cube.mesh_dim`, but only if their +:class:`~iris.cube.Cube.mesh`\es are *equal* (in practice, identical, even to +matching ``var_name``\s). .. seealso:: You may wish to investigate - :func:`iris.experimental.ugrid.recombine_submeshes`, which can be used - for a very specific type of :class:`~iris.experimental.ugrid.MeshXY` + :func:`iris.mesh.recombine_submeshes`, which can be used + for a very specific type of :class:`~iris.mesh.MeshXY` combination not detailed here. Arithmetic @@ -869,7 +857,7 @@ Arithmetic Cube Arithmetic (described in :doc:`/userguide/cube_maths`) has been extended to handle :class:`~iris.cube.Cube`\s that include -:class:`~iris.experimental.ugrid.MeshCoord`\s, and hence have a ``cube.mesh``. +:class:`~iris.mesh.MeshCoord`\s, and hence have a ``cube.mesh``. Cubes with meshes can be combined in arithmetic operations like "ordinary" cubes. They can combine with other cubes without that mesh diff --git a/docs/src/further_topics/ugrid/other_meshes.rst b/docs/src/further_topics/ugrid/other_meshes.rst index df83c8c4f6..19f220be82 100644 --- a/docs/src/further_topics/ugrid/other_meshes.rst +++ b/docs/src/further_topics/ugrid/other_meshes.rst @@ -25,7 +25,7 @@ A FESOM mesh encoded in a NetCDF file includes: To represent the Voronoi Polygons as faces, the corner coordinates will be used as the **nodes** when creating the Iris -:class:`~iris.experimental.ugrid.mesh.MeshXY`. +:class:`~iris.ugrid.mesh.MeshXY`. .. dropdown:: Code :icon: code @@ -33,7 +33,7 @@ as the **nodes** when creating the Iris .. code-block:: python >>> import iris - >>> from iris.experimental.ugrid import MeshXY + >>> from iris.ugrid import MeshXY >>> temperature_cube = iris.load_cube("my_file.nc", "sea_surface_temperature") @@ -113,7 +113,7 @@ An SMC grid encoded in a NetCDF file includes: From this information we can derive face corner coordinates, which will be used as the **nodes** when creating the Iris -:class:`~iris.experimental.ugrid.mesh.MeshXY`. +:class:`~iris.ugrid.mesh.MeshXY`. .. dropdown:: Code @@ -122,7 +122,7 @@ as the **nodes** when creating the Iris .. code-block:: python >>> import iris - >>> from iris.experimental.ugrid import MeshXY + >>> from iris.ugrid import MeshXY >>> import numpy as np @@ -265,7 +265,7 @@ dimensions into a single mesh dimension. Since Iris cubes don't support a "resh >>> import iris >>> from iris.coords import AuxCoord, CellMeasure >>> from iris.cube import Cube - >>> from iris.experimental.ugrid.mesh import MeshXY, Connectivity + >>> from iris.ugrid.mesh import MeshXY, Connectivity >>> filepath = iris.sample_data_path('orca2_votemper.nc') diff --git a/docs/src/further_topics/ugrid/partner_packages.rst b/docs/src/further_topics/ugrid/partner_packages.rst index 87a61ae0fe..f69546446c 100644 --- a/docs/src/further_topics/ugrid/partner_packages.rst +++ b/docs/src/further_topics/ugrid/partner_packages.rst @@ -58,7 +58,7 @@ PyVista is described as "VTK for humans" - VTK is a very powerful toolkit for working with meshes, and PyVista brings that power into the Python ecosystem. GeoVista in turn makes it easy to use PyVista specifically for cartographic work, designed from the start with the Iris -:class:`~iris.experimental.ugrid.MeshXY` in mind. +:class:`~iris.mesh.MeshXY` in mind. Applications ------------ diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 2976ae56ca..e04b832e23 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -88,6 +88,13 @@ This document explains the changes made to Iris for this release :meth:`~iris.experimental.ugrid.Mesh.coord`, :meth:`~iris.experimental.ugrid.Mesh.coords` and :meth:`~iris.experimental.ugrid.Mesh.remove_coords`. (:pull:`6055`) +#. `@pp-mo`_ moved all the mesh API from the :mod:`iris.experimental.ugrid` module to + to :mod:`iris.mesh`, making this public supported API. Note that the + :class:`iris.experimental.ugrid.Mesh` class is renamed as :class:`iris.mesh.MeshXY`, + to allow for possible future mesh types with different properties to exist as + subclasses of a common generic :class:`~iris.mesh.components.Mesh` class. + (:issue:`6057`, :pull:`6061`, :pull:`6077`) + 🚀 Performance Enhancements =========================== diff --git a/lib/iris/analysis/_regrid.py b/lib/iris/analysis/_regrid.py index 6c10b8c404..431871de2c 100644 --- a/lib/iris/analysis/_regrid.py +++ b/lib/iris/analysis/_regrid.py @@ -997,8 +997,7 @@ def _create_cube(data, src, src_dims, tgt_coords, num_tgt_dims, regrid_callback) src_dims : tuple of int The dimensions of the X and Y coordinate within the source Cube. tgt_coords : tuple of :class:`iris.coords.Coord - Either two 1D :class:`iris.coords.DimCoord`, two 1D - :class:`iris.experimental.ugrid.DimCoord` or two n-D + Either two 1D :class:`iris.coords.DimCoord`, or two n-D :class:`iris.coords.AuxCoord` representing the new grid's X and Y coordinates. num_tgt_dims : int diff --git a/lib/iris/common/metadata.py b/lib/iris/common/metadata.py index e11ea71462..6f3e455b4d 100644 --- a/lib/iris/common/metadata.py +++ b/lib/iris/common/metadata.py @@ -1334,6 +1334,357 @@ def equal(self, other, lenient=None): return super().equal(other, lenient=lenient) +class ConnectivityMetadata(BaseMetadata): + """Metadata container for a :class:`~iris.mesh.Connectivity`.""" + + # The "location_axis" member is stateful only, and does not participate in + # lenient/strict equivalence. + _members = ("cf_role", "start_index", "location_axis") + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def _combine_lenient(self, other): + """Perform lenient combination of metadata members for connectivities. + + Parameters + ---------- + other : ConnectivityMetadata + The other connectivity metadata participating in the lenient + combination. + + Returns + ------- + A list of combined metadata member values. + + """ + + # Perform "strict" combination for "cf_role", "start_index", "location_axis". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return left if left == right else None + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in ConnectivityMetadata._members] + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.extend(values) + + return result + + def _compare_lenient(self, other): + """Perform lenient equality of metadata members for connectivities. + + Parameters + ---------- + other : ConnectivityMetadata + The other connectivity metadata participating in the lenient + comparison. + + Returns + ------- + bool + + """ + # Perform "strict" comparison for "cf_role", "start_index". + # The "location_axis" member is not part of lenient equivalence. + members = filter( + lambda member: member != "location_axis", + ConnectivityMetadata._members, + ) + result = all( + [getattr(self, field) == getattr(other, field) for field in members] + ) + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """Perform lenient difference of metadata members for connectivities. + + Parameters + ---------- + other : ConnectivityMetadata + The other connectivity metadata participating in the lenient + difference. + + Returns + ------- + A list of difference metadata member values. + + """ + + # Perform "strict" difference for "cf_role", "start_index", "location_axis". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return None if left == right else (left, right) + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in ConnectivityMetadata._members] + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.extend(values) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + +class MeshMetadata(BaseMetadata): + """Metadata container for a :class:`~iris.mesh.MeshXY`.""" + + # The node_dimension", "edge_dimension" and "face_dimension" members are + # stateful only; they not participate in lenient/strict equivalence. + _members = ( + "topology_dimension", + "node_dimension", + "edge_dimension", + "face_dimension", + ) + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def _combine_lenient(self, other): + """Perform lenient combination of metadata members for meshes. + + Parameters + ---------- + other : MeshMetadata + The other mesh metadata participating in the lenient + combination. + + Returns + ------- + A list of combined metadata member values. + + """ + + # Perform "strict" combination for "topology_dimension", + # "node_dimension", "edge_dimension" and "face_dimension". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return left if left == right else None + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in MeshMetadata._members] + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.extend(values) + + return result + + def _compare_lenient(self, other): + """Perform lenient equality of metadata members for meshes. + + Parameters + ---------- + other : MeshMetadata + The other mesh metadata participating in the lenient + comparison. + + Returns + ------- + bool + + """ + # Perform "strict" comparison for "topology_dimension". + # "node_dimension", "edge_dimension" and "face_dimension" are not part + # of lenient equivalence at all. + result = self.topology_dimension == other.topology_dimension + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """Perform lenient difference of metadata members for meshes. + + Parameters + ---------- + other : MeshMetadata + The other mesh metadata participating in the lenient + difference. + + Returns + ------- + A list of difference metadata member values. + + """ + + # Perform "strict" difference for "topology_dimension", + # "node_dimension", "edge_dimension" and "face_dimension". + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return None if left == right else (left, right) + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in MeshMetadata._members] + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.extend(values) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + +class MeshCoordMetadata(BaseMetadata): + """Metadata container for a :class:`~iris.coords.MeshCoord`.""" + + _members = ("location", "axis") + # NOTE: in future, we may add 'mesh' as part of this metadata, + # as the MeshXY seems part of the 'identity' of a MeshCoord. + # For now we omit it, particularly as we don't yet implement MeshXY.__eq__. + # + # Thus, for now, the MeshCoord class will need to handle 'mesh' explicitly + # in identity / comparison, but in future that may be simplified. + + __slots__ = () + + @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) + @lenient_service + def __eq__(self, other): + return super().__eq__(other) + + def _combine_lenient(self, other): + """Perform lenient combination of metadata members for MeshCoord. + + Parameters + ---------- + other : MeshCoordMetadata + The other metadata participating in the lenient combination. + + Returns + ------- + A list of combined metadata member values. + + """ + + # It is actually "strict" : return None except where members are equal. + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return left if left == right else None + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in self._members] + # Perform lenient combination of the other parent members. + result = super()._combine_lenient(other) + result.extend(values) + + return result + + def _compare_lenient(self, other): + """Perform lenient equality of metadata members for MeshCoord. + + Parameters + ---------- + other : MeshCoordMetadata + The other metadata participating in the lenient comparison. + + Returns + ------- + bool + + """ + # Perform "strict" comparison for the MeshCoord specific members + # 'location', 'axis' : for equality, they must all match. + result = all( + [getattr(self, field) == getattr(other, field) for field in self._members] + ) + if result: + # Perform lenient comparison of the other parent members. + result = super()._compare_lenient(other) + + return result + + def _difference_lenient(self, other): + """Perform lenient difference of metadata members for MeshCoord. + + Parameters + ---------- + other : MeshCoordMetadata + The other MeshCoord metadata participating in the lenient + difference. + + Returns + ------- + A list of different metadata member values. + + """ + + # Perform "strict" difference for location / axis. + def func(field): + left = getattr(self, field) + right = getattr(other, field) + return None if left == right else (left, right) + + # Note that, we use "_members" not "_fields". + values = [func(field) for field in self._members] + # Perform lenient difference of the other parent members. + result = super()._difference_lenient(other) + result.extend(values) + + return result + + @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) + @lenient_service + def combine(self, other, lenient=None): + return super().combine(other, lenient=lenient) + + @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) + @lenient_service + def difference(self, other, lenient=None): + return super().difference(other, lenient=lenient) + + @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) + @lenient_service + def equal(self, other, lenient=None): + return super().equal(other, lenient=lenient) + + def metadata_filter( instances, item=None, @@ -1601,45 +1952,54 @@ def metadata_manager_factory(cls, **kwargs): #: Convenience collection of lenient metadata combine services. -# TODO: change lists back to tuples once CellMeasureMetadata is re-integrated -# here (currently in experimental.ugrid). -SERVICES_COMBINE = [ +SERVICES_COMBINE = ( AncillaryVariableMetadata.combine, BaseMetadata.combine, CellMeasureMetadata.combine, + ConnectivityMetadata.combine, CoordMetadata.combine, CubeMetadata.combine, DimCoordMetadata.combine, -] + MeshCoordMetadata.combine, + MeshMetadata.combine, +) #: Convenience collection of lenient metadata difference services. -SERVICES_DIFFERENCE = [ +SERVICES_DIFFERENCE = ( AncillaryVariableMetadata.difference, BaseMetadata.difference, CellMeasureMetadata.difference, + ConnectivityMetadata.difference, CoordMetadata.difference, CubeMetadata.difference, DimCoordMetadata.difference, -] + MeshCoordMetadata.difference, + MeshMetadata.difference, +) #: Convenience collection of lenient metadata equality services. -SERVICES_EQUAL = [ +SERVICES_EQUAL = ( AncillaryVariableMetadata.__eq__, AncillaryVariableMetadata.equal, BaseMetadata.__eq__, BaseMetadata.equal, CellMeasureMetadata.__eq__, CellMeasureMetadata.equal, + ConnectivityMetadata.__eq__, + ConnectivityMetadata.equal, CoordMetadata.__eq__, CoordMetadata.equal, CubeMetadata.__eq__, CubeMetadata.equal, DimCoordMetadata.__eq__, DimCoordMetadata.equal, -] - + MeshCoordMetadata.__eq__, + MeshCoordMetadata.equal, + MeshMetadata.__eq__, + MeshMetadata.equal, +) #: Convenience collection of lenient metadata services. SERVICES = SERVICES_COMBINE + SERVICES_DIFFERENCE + SERVICES_EQUAL diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index 8b5f0cdc7f..87ad05791b 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -71,7 +71,7 @@ class _PreparedItem: axis: Any = None def create_coord(self, metadata): - from iris.experimental.ugrid.mesh import MeshCoord + from iris.mesh import MeshCoord if issubclass(self.container, MeshCoord): # Make a MeshCoord, for which we have mesh/location/axis. @@ -741,7 +741,7 @@ def _create_prepared_item( if container is None: container = type(coord) - from iris.experimental.ugrid.mesh import MeshCoord + from iris.mesh import MeshCoord if issubclass(container, MeshCoord): # Build a prepared-item to make a MeshCoord. diff --git a/lib/iris/coords.py b/lib/iris/coords.py index e563b56498..a56c13d9af 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -804,6 +804,9 @@ def xml_element(self, doc): :class:`_DimensionalMetadata`. """ + # deferred import to avoid possible circularity + from iris.mesh import Connectivity + # Create the XML element as the camelCaseEquivalent of the # class name. element_name = type(self).__name__ @@ -843,9 +846,7 @@ def xml_element(self, doc): # otherwise. if isinstance(self, Coord): values_term = "points" - # TODO: replace with isinstance(self, Connectivity) once Connectivity - # is re-integrated here (currently in experimental.ugrid). - elif hasattr(self, "indices"): + elif isinstance(self, Connectivity): values_term = "indices" else: values_term = "data" diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 47b66b6ead..54e086937d 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2088,7 +2088,7 @@ def coords( If ``None``, returns all coordinates. mesh_coords : optional Set to ``True`` to return only coordinates which are - :class:`~iris.experimental.ugrid.MeshCoord`\'s. + :class:`~iris.mesh.MeshCoord`\'s. Set to ``False`` to return only non-mesh coordinates. If ``None``, returns all coordinates. @@ -2115,7 +2115,7 @@ def coords( 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, + # Use duck typing to avoid importing from iris.mesh, # which could be a circular import. if mesh_coords: # *only* MeshCoords @@ -2245,7 +2245,7 @@ def coord( If ``None``, returns all coordinates. mesh_coords : optional Set to ``True`` to return only coordinates which are - :class:`~iris.experimental.ugrid.MeshCoord`\'s. + :class:`~iris.mesh.MeshCoord`\'s. Set to ``False`` to return only non-mesh coordinates. If ``None``, returns all coordinates. @@ -2365,18 +2365,18 @@ def _any_meshcoord(self): @property def mesh(self): - r"""Return the unstructured :class:`~iris.experimental.ugrid.MeshXY` associated with the cube. + r"""Return the unstructured :class:`~iris.mesh.MeshXY` associated with the cube. - Return the unstructured :class:`~iris.experimental.ugrid.MeshXY` + Return the unstructured :class:`~iris.mesh.MeshXY` associated with the cube, if the cube has any - :class:`~iris.experimental.ugrid.MeshCoord`, + :class:`~iris.mesh.MeshCoord`, or ``None`` if it has none. Returns ------- - :class:`iris.experimental.ugrid.mesh.MeshXY` or None + :class:`iris.mesh.MeshXY` or None The mesh of the cube - :class:`~iris.experimental.ugrid.MeshCoord`'s, + :class:`~iris.mesh.MeshCoord`'s, or ``None``. """ @@ -2390,14 +2390,14 @@ def location(self): r"""Return the mesh "location" of the cube data. Return the mesh "location" of the cube data, if the cube has any - :class:`~iris.experimental.ugrid.MeshCoord`, + :class:`~iris.mesh.MeshCoord`, or ``None`` if it has none. Returns ------- str or None The mesh location of the cube - :class:`~iris.experimental.ugrid.MeshCoords` + :class:`~iris.mesh.MeshCoords` (i.e. one of 'face' / 'edge' / 'node'), or ``None``. """ @@ -2410,14 +2410,14 @@ def mesh_dim(self): r"""Return the cube dimension of the mesh. Return the cube dimension of the mesh, if the cube has any - :class:`~iris.experimental.ugrid.MeshCoord`, + :class:`~iris.mesh.MeshCoord`, or ``None`` if it has none. Returns ------- int or None The cube dimension which the cube - :class:`~iris.experimental.ugrid.MeshCoord` map to, + :class:`~iris.mesh.MeshCoord` map to, or ``None``. """ diff --git a/lib/iris/experimental/geovista.py b/lib/iris/experimental/geovista.py index 3f42f09e03..07413f1529 100644 --- a/lib/iris/experimental/geovista.py +++ b/lib/iris/experimental/geovista.py @@ -8,7 +8,7 @@ from geovista.common import VTK_CELL_IDS, VTK_POINT_IDS from iris.exceptions import CoordinateNotFoundError -from iris.experimental.ugrid import MeshXY +from iris.mesh import MeshXY def _get_coord(cube, axis): @@ -52,7 +52,7 @@ def cube_to_polydata(cube, **kwargs): If a :class:`~iris.cube.Cube` with too many dimensions is passed. Only the horizontal data can be represented, meaning a 2D Cube, or 1D Cube if the horizontal space is described by - :class:`~iris.experimental.ugrid.MeshCoord`\ s. + :class:`~iris.mesh.MeshCoord`\ s. Examples -------- diff --git a/lib/iris/experimental/ugrid.py b/lib/iris/experimental/ugrid.py new file mode 100644 index 0000000000..6e036ad96e --- /dev/null +++ b/lib/iris/experimental/ugrid.py @@ -0,0 +1,155 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. + +"""Legacy import location for mesh support. + +See :mod:`iris.mesh` for the new, correct import location. + +Notes +----- +This import path alios is provided for backwards compatibility, but will be removed +in a future release : Please re-write code to import from the new module path. + +This legacy import module will be removed in a future release. +N.B. it does **not** need to wait for a major release, since the former API was +experimental. + +.. deprecated:: 3.10 + All the former :mod:`iris.experimental.mesh` modules have been relocated to + :mod:`iris.mesh` and its submodules. Please re-write code to import from the new + module path, and replace any 'iris.experimental.ugrid.Mesh' with + 'iris.mesh.MeshXY'. + + This import path alias is provided for backwards compatibility, but will be removed + in a future release : N.B. removing this does **not** need to wait for a major + release, since the API is experimental. + +""" + +from __future__ import annotations + +from contextlib import contextmanager +import threading + +from .._deprecation import warn_deprecated +from ..mesh import Connectivity as _Connectivity +from ..mesh import MeshCoord as _MeshCoord +from ..mesh import MeshXY as _MeshXY +from ..mesh import load_mesh, load_meshes, recombine_submeshes, save_mesh + + +# NOTE: publishing the original Mesh, MeshCoord and Connectivity here causes a Sphinx +# Sphinx warning, E.G.: +# "WARNING: duplicate object description of iris.mesh.Mesh, other instance +# in generated/api/iris.experimental.mesh, use :no-index: for one of them" +# For some reason, this only happens for the classes, and not the functions. +# +# This is a fatal problem, i.e. breaks the build since we are building with -W. +# We couldn't fix this with "autodoc_suppress_warnings", so the solution for now is to +# wrap the classes. Which is really ugly. +class Mesh(_MeshXY): + pass + + +class MeshCoord(_MeshCoord): + pass + + +class Connectivity(_Connectivity): + pass + + +class ParseUGridOnLoad(threading.local): + def __init__(self): + """Thead-safe state to enable UGRID-aware NetCDF loading. + + A flag for dictating whether to use the experimental UGRID-aware + version of Iris NetCDF loading. Object is thread-safe. + + Use via the run-time switch + :const:`~iris.mesh.load.PARSE_UGRID_ON_LOAD`. + Use :meth:`context` to temporarily activate. + + Notes + ----- + .. deprecated:: 1.10 + Do not use -- due to be removed at next major release : + UGRID loading is now **always** active for files containing a UGRID mesh. + + """ + + def __bool__(self): + return True + + @contextmanager + def context(self): + """Activate UGRID-aware NetCDF loading. + + Use the standard Iris loading API while within the context manager. If + the loaded file(s) include any UGRID content, this will be parsed and + attached to the resultant cube(s) accordingly. + + Use via the run-time switch + :const:`~iris.mesh.load.PARSE_UGRID_ON_LOAD`. + + For example:: + + with PARSE_UGRID_ON_LOAD.context(): + my_cube_list = iris.load([my_file_path, my_file_path2], + constraint=my_constraint, + callback=my_callback) + + Notes + ----- + .. deprecated:: 1.10 + Do not use -- due to be removed at next major release : + UGRID loading is now **always** active for files containing a UGRID mesh. + + Examples + -------- + Replace usage, for example: + + .. code-block:: python + + with iris.experimental.mesh.PARSE_UGRID_ON_LOAD.context(): + mesh_cubes = iris.load(path) + + with: + + .. code-block:: python + + mesh_cubes = iris.load(path) + + """ + wmsg = ( + "iris.experimental.mesh.load.PARSE_UGRID_ON_LOAD has been deprecated " + "and will be removed. Please remove all uses : these are no longer needed, " + "as UGRID loading is now applied to any file containing a mesh." + ) + warn_deprecated(wmsg) + yield + + +#: Run-time switch for experimental UGRID-aware NetCDF loading. See :class:`~iris.mesh.load.ParseUGridOnLoad`. +PARSE_UGRID_ON_LOAD = ParseUGridOnLoad() + + +__all__ = [ + "Connectivity", + "Mesh", + "MeshCoord", + "PARSE_UGRID_ON_LOAD", + "load_mesh", + "load_meshes", + "recombine_submeshes", + "save_mesh", +] + +warn_deprecated( + "All the former :mod:`iris.experimental.mesh` modules have been relocated to " + "module 'iris.mesh' and its submodules. " + "Please re-write code to import from the new module path, and replace any " + "'iris.experimental.ugrid.Mesh' with 'iris.mesh.MeshXY'." +) diff --git a/lib/iris/experimental/ugrid/cf.py b/lib/iris/experimental/ugrid/cf.py deleted file mode 100644 index 281bdba878..0000000000 --- a/lib/iris/experimental/ugrid/cf.py +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. - -"""Extensions to Iris' CF variable representation to represent CF UGrid variables. - -Eventual destination: :mod:`iris.fileformats.cf`. - -""" - -import warnings - -from ...fileformats import cf -from ...warnings import IrisCfLabelVarWarning, IrisCfMissingVarWarning -from .mesh import Connectivity - - -class CFUGridConnectivityVariable(cf.CFVariable): - """A CF_UGRID connectivity variable. - - A CF_UGRID connectivity variable points to an index variable identifying - for every element (edge/face/volume) the indices of its corner nodes. The - connectivity array will thus be a matrix of size n-elements x n-corners. - For the indexing one may use either 0- or 1-based indexing; the convention - used should be specified using a ``start_index`` attribute to the index - variable. - - For face elements: the corner nodes should be specified in anticlockwise - direction as viewed from above. For volume elements: use the - additional attribute ``volume_shape_type`` which points to a flag variable - that specifies for every volume its shape. - - Identified by a CF-netCDF variable attribute equal to any one of the values - in :attr:`~iris.experimental.ugrid.mesh.Connectivity.UGRID_CF_ROLES`. - - .. seealso:: - - The UGRID Conventions, https://ugrid-conventions.github.io/ugrid-conventions/ - - """ - - cf_identity = NotImplemented - cf_identities = Connectivity.UGRID_CF_ROLES - - @classmethod - def identify(cls, variables, ignore=None, target=None, warn=True): - result = {} - ignore, target = cls._identify_common(variables, ignore, target) - - # Identify all CF-UGRID connectivity variables. - for nc_var_name, nc_var in target.items(): - # Check for connectivity variable references, iterating through - # the valid cf roles. - for identity in cls.cf_identities: - nc_var_att = getattr(nc_var, identity, None) - - if nc_var_att is not None: - # UGRID only allows for one of each connectivity cf role. - name = nc_var_att.strip() - if name not in ignore: - if name not in variables: - message = ( - f"Missing CF-UGRID connectivity variable " - f"{name}, referenced by netCDF variable " - f"{nc_var_name}" - ) - if warn: - warnings.warn(message, category=IrisCfMissingVarWarning) - else: - # Restrict to non-string type i.e. not a - # CFLabelVariable. - if not cf._is_str_dtype(variables[name]): - result[name] = CFUGridConnectivityVariable( - name, variables[name] - ) - else: - message = ( - f"Ignoring variable {name}, identified " - f"as a CF-UGRID connectivity - is a " - f"CF-netCDF label variable." - ) - if warn: - warnings.warn( - message, category=IrisCfLabelVarWarning - ) - - return result - - -class CFUGridAuxiliaryCoordinateVariable(cf.CFVariable): - """A CF-UGRID auxiliary coordinate variable. - - A CF-UGRID auxiliary coordinate variable is a CF-netCDF auxiliary - coordinate variable representing the element (node/edge/face/volume) - locations (latitude, longitude or other spatial coordinates, and optional - elevation or other coordinates). These auxiliary coordinate variables will - have length n-elements. - - For elements other than nodes, these auxiliary coordinate variables may - have in turn a ``bounds`` attribute that specifies the bounding coordinates - of the element (thereby duplicating the data in the ``node_coordinates`` - variables). - - Identified by the CF-netCDF variable attribute - ``node_``/``edge_``/``face_``/``volume_coordinates``. - - .. seealso:: - - The UGRID Conventions, https://ugrid-conventions.github.io/ugrid-conventions/ - - """ - - cf_identity = NotImplemented - cf_identities = [ - "node_coordinates", - "edge_coordinates", - "face_coordinates", - "volume_coordinates", - ] - - @classmethod - def identify(cls, variables, ignore=None, target=None, warn=True): - result = {} - ignore, target = cls._identify_common(variables, ignore, target) - - # Identify any CF-UGRID-relevant auxiliary coordinate variables. - for nc_var_name, nc_var in target.items(): - # Check for UGRID auxiliary coordinate variable references. - for identity in cls.cf_identities: - nc_var_att = getattr(nc_var, identity, None) - - if nc_var_att is not None: - for name in nc_var_att.split(): - if name not in ignore: - if name not in variables: - message = ( - f"Missing CF-netCDF auxiliary coordinate " - f"variable {name}, referenced by netCDF " - f"variable {nc_var_name}" - ) - if warn: - warnings.warn( - message, - category=IrisCfMissingVarWarning, - ) - else: - # Restrict to non-string type i.e. not a - # CFLabelVariable. - if not cf._is_str_dtype(variables[name]): - result[name] = CFUGridAuxiliaryCoordinateVariable( - name, variables[name] - ) - else: - message = ( - f"Ignoring variable {name}, " - f"identified as a CF-netCDF " - f"auxiliary coordinate - is a " - f"CF-netCDF label variable." - ) - if warn: - warnings.warn( - message, - category=IrisCfLabelVarWarning, - ) - - return result - - -class CFUGridMeshVariable(cf.CFVariable): - """A CF-UGRID mesh variable is a dummy variable for storing topology information as attributes. - - A CF-UGRID mesh variable is a dummy variable for storing topology - information as attributes. The mesh variable has the ``cf_role`` - 'mesh_topology'. - - The UGRID conventions describe define the mesh topology as the - interconnection of various geometrical elements of the mesh. The pure - interconnectivity is independent of georeferencing the individual - geometrical elements, but for the practical applications for which the - UGRID CF extension is defined, coordinate data will always be added. - - Identified by the CF-netCDF variable attribute 'mesh'. - - .. seealso:: - - The UGRID Conventions, https://ugrid-conventions.github.io/ugrid-conventions/ - - """ - - cf_identity = "mesh" - - @classmethod - def identify(cls, variables, ignore=None, target=None, warn=True): - result = {} - ignore, target = cls._identify_common(variables, ignore, target) - - # Identify all CF-UGRID mesh variables. - all_vars = target == variables - for nc_var_name, nc_var in target.items(): - if all_vars: - # SPECIAL BEHAVIOUR FOR MESH VARIABLES. - # We are looking for all mesh variables. Check if THIS variable - # is a mesh using its own attributes. - if getattr(nc_var, "cf_role", "") == "mesh_topology": - result[nc_var_name] = CFUGridMeshVariable(nc_var_name, nc_var) - - # Check for mesh variable references. - nc_var_att = getattr(nc_var, cls.cf_identity, None) - - if nc_var_att is not None: - # UGRID only allows for 1 mesh per variable. - name = nc_var_att.strip() - if name not in ignore: - if name not in variables: - message = ( - f"Missing CF-UGRID mesh variable {name}, " - f"referenced by netCDF variable {nc_var_name}" - ) - if warn: - warnings.warn(message, category=IrisCfMissingVarWarning) - else: - # Restrict to non-string type i.e. not a - # CFLabelVariable. - if not cf._is_str_dtype(variables[name]): - result[name] = CFUGridMeshVariable(name, variables[name]) - else: - message = ( - f"Ignoring variable {name}, identified as a " - f"CF-UGRID mesh - is a CF-netCDF label " - f"variable." - ) - if warn: - warnings.warn(message, category=IrisCfLabelVarWarning) - - return result - - -class CFUGridGroup(cf.CFGroup): - """Represents a collection of CF Metadata Conventions variables and netCDF global attributes. - - Represents a collection of 'NetCDF Climate and Forecast (CF) Metadata - Conventions' variables and netCDF global attributes. - - Specialisation of :class:`~iris.fileformats.cf.CFGroup` that includes extra - collections for CF-UGRID-specific variable types. - - """ - - @property - def connectivities(self): - """Collection of CF-UGRID connectivity variables.""" - return self._cf_getter(CFUGridConnectivityVariable) - - @property - def ugrid_coords(self): - """Collection of CF-UGRID-relevant auxiliary coordinate variables.""" - return self._cf_getter(CFUGridAuxiliaryCoordinateVariable) - - @property - def meshes(self): - """Collection of CF-UGRID mesh variables.""" - return self._cf_getter(CFUGridMeshVariable) - - @property - def non_data_variable_names(self): - """:class:`set` of names of the CF-netCDF/CF-UGRID variables that are not the data pay-load.""" - extra_variables = (self.connectivities, self.ugrid_coords, self.meshes) - extra_result = set() - for variable in extra_variables: - extra_result |= set(variable) - return super().non_data_variable_names | extra_result - - -class CFUGridReader(cf.CFReader): - """Allows the contents of a netCDF file to be. - - This class allows the contents of a netCDF file to be interpreted according - to the 'NetCDF Climate and Forecast (CF) Metadata Conventions'. - - Specialisation of :class:`~iris.fileformats.cf.CFReader` that can also - handle CF-UGRID-specific variable types. - - """ - - _variable_types = cf.CFReader._variable_types + ( # type: ignore[assignment] - CFUGridConnectivityVariable, - CFUGridAuxiliaryCoordinateVariable, - CFUGridMeshVariable, - ) - - CFGroup = CFUGridGroup diff --git a/lib/iris/experimental/ugrid/metadata.py b/lib/iris/experimental/ugrid/metadata.py deleted file mode 100644 index 28b9f413d9..0000000000 --- a/lib/iris/experimental/ugrid/metadata.py +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. - -"""The common metadata API classes for :mod:`iris.experimental.ugrid.mesh`. - -Eventual destination: :mod:`iris.common.metadata`. - -""" - -from functools import wraps - -from ...common import BaseMetadata -from ...common.lenient import _lenient_service as lenient_service -from ...common.metadata import ( - SERVICES, - SERVICES_COMBINE, - SERVICES_DIFFERENCE, - SERVICES_EQUAL, -) - - -class ConnectivityMetadata(BaseMetadata): - """Metadata container for a :class:`~iris.experimental.ugrid.mesh.Connectivity`.""" - - # The "location_axis" member is stateful only, and does not participate in - # lenient/strict equivalence. - _members = ("cf_role", "start_index", "location_axis") - - __slots__ = () - - @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) - @lenient_service - def __eq__(self, other): - return super().__eq__(other) - - def _combine_lenient(self, other): - """Perform lenient combination of metadata members for connectivities. - - Parameters - ---------- - other : ConnectivityMetadata - The other connectivity metadata participating in the lenient - combination. - - Returns - ------- - A list of combined metadata member values. - - """ - - # Perform "strict" combination for "cf_role", "start_index", "location_axis". - def func(field): - left = getattr(self, field) - right = getattr(other, field) - return left if left == right else None - - # Note that, we use "_members" not "_fields". - values = [func(field) for field in ConnectivityMetadata._members] - # Perform lenient combination of the other parent members. - result = super()._combine_lenient(other) - result.extend(values) - - return result - - def _compare_lenient(self, other): - """Perform lenient equality of metadata members for connectivities. - - Parameters - ---------- - other : ConnectivityMetadata - The other connectivity metadata participating in the lenient - comparison. - - Returns - ------- - bool - - """ - # Perform "strict" comparison for "cf_role", "start_index". - # The "location_axis" member is not part of lenient equivalence. - members = filter( - lambda member: member != "location_axis", - ConnectivityMetadata._members, - ) - result = all( - [getattr(self, field) == getattr(other, field) for field in members] - ) - if result: - # Perform lenient comparison of the other parent members. - result = super()._compare_lenient(other) - - return result - - def _difference_lenient(self, other): - """Perform lenient difference of metadata members for connectivities. - - Parameters - ---------- - other : ConnectivityMetadata - The other connectivity metadata participating in the lenient - difference. - - Returns - ------- - A list of difference metadata member values. - - """ - - # Perform "strict" difference for "cf_role", "start_index", "location_axis". - def func(field): - left = getattr(self, field) - right = getattr(other, field) - return None if left == right else (left, right) - - # Note that, we use "_members" not "_fields". - values = [func(field) for field in ConnectivityMetadata._members] - # Perform lenient difference of the other parent members. - result = super()._difference_lenient(other) - result.extend(values) - - return result - - @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) - @lenient_service - def combine(self, other, lenient=None): - return super().combine(other, lenient=lenient) - - @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) - @lenient_service - def difference(self, other, lenient=None): - return super().difference(other, lenient=lenient) - - @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) - @lenient_service - def equal(self, other, lenient=None): - return super().equal(other, lenient=lenient) - - -class MeshMetadata(BaseMetadata): - """Metadata container for a :class:`~iris.experimental.ugrid.mesh.MeshXY`.""" - - # The node_dimension", "edge_dimension" and "face_dimension" members are - # stateful only; they not participate in lenient/strict equivalence. - _members = ( - "topology_dimension", - "node_dimension", - "edge_dimension", - "face_dimension", - ) - - __slots__ = () - - @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) - @lenient_service - def __eq__(self, other): - return super().__eq__(other) - - def _combine_lenient(self, other): - """Perform lenient combination of metadata members for meshes. - - Parameters - ---------- - other : MeshMetadata - The other mesh metadata participating in the lenient - combination. - - Returns - ------- - A list of combined metadata member values. - - """ - - # Perform "strict" combination for "topology_dimension", - # "node_dimension", "edge_dimension" and "face_dimension". - def func(field): - left = getattr(self, field) - right = getattr(other, field) - return left if left == right else None - - # Note that, we use "_members" not "_fields". - values = [func(field) for field in MeshMetadata._members] - # Perform lenient combination of the other parent members. - result = super()._combine_lenient(other) - result.extend(values) - - return result - - def _compare_lenient(self, other): - """Perform lenient equality of metadata members for meshes. - - Parameters - ---------- - other : MeshMetadata - The other mesh metadata participating in the lenient - comparison. - - Returns - ------- - bool - - """ - # Perform "strict" comparison for "topology_dimension". - # "node_dimension", "edge_dimension" and "face_dimension" are not part - # of lenient equivalence at all. - result = self.topology_dimension == other.topology_dimension - if result: - # Perform lenient comparison of the other parent members. - result = super()._compare_lenient(other) - - return result - - def _difference_lenient(self, other): - """Perform lenient difference of metadata members for meshes. - - Parameters - ---------- - other : MeshMetadata - The other mesh metadata participating in the lenient - difference. - - Returns - ------- - A list of difference metadata member values. - - """ - - # Perform "strict" difference for "topology_dimension", - # "node_dimension", "edge_dimension" and "face_dimension". - def func(field): - left = getattr(self, field) - right = getattr(other, field) - return None if left == right else (left, right) - - # Note that, we use "_members" not "_fields". - values = [func(field) for field in MeshMetadata._members] - # Perform lenient difference of the other parent members. - result = super()._difference_lenient(other) - result.extend(values) - - return result - - @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) - @lenient_service - def combine(self, other, lenient=None): - return super().combine(other, lenient=lenient) - - @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) - @lenient_service - def difference(self, other, lenient=None): - return super().difference(other, lenient=lenient) - - @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) - @lenient_service - def equal(self, other, lenient=None): - return super().equal(other, lenient=lenient) - - -class MeshCoordMetadata(BaseMetadata): - """Metadata container for a :class:`~iris.coords.MeshCoord`.""" - - _members = ("location", "axis") - # NOTE: in future, we may add 'mesh' as part of this metadata, - # as the MeshXY seems part of the 'identity' of a MeshCoord. - # For now we omit it, particularly as we don't yet implement MeshXY.__eq__. - # - # Thus, for now, the MeshCoord class will need to handle 'mesh' explicitly - # in identity / comparison, but in future that may be simplified. - - __slots__ = () - - @wraps(BaseMetadata.__eq__, assigned=("__doc__",), updated=()) - @lenient_service - def __eq__(self, other): - return super().__eq__(other) - - def _combine_lenient(self, other): - """Perform lenient combination of metadata members for MeshCoord. - - Parameters - ---------- - other : MeshCoordMetadata - The other metadata participating in the lenient combination. - - Returns - ------- - A list of combined metadata member values. - - """ - - # It is actually "strict" : return None except where members are equal. - def func(field): - left = getattr(self, field) - right = getattr(other, field) - return left if left == right else None - - # Note that, we use "_members" not "_fields". - values = [func(field) for field in self._members] - # Perform lenient combination of the other parent members. - result = super()._combine_lenient(other) - result.extend(values) - - return result - - def _compare_lenient(self, other): - """Perform lenient equality of metadata members for MeshCoord. - - Parameters - ---------- - other : MeshCoordMetadata - The other metadata participating in the lenient comparison. - - Returns - ------- - bool - - """ - # Perform "strict" comparison for the MeshCoord specific members - # 'location', 'axis' : for equality, they must all match. - result = all( - [getattr(self, field) == getattr(other, field) for field in self._members] - ) - if result: - # Perform lenient comparison of the other parent members. - result = super()._compare_lenient(other) - - return result - - def _difference_lenient(self, other): - """Perform lenient difference of metadata members for MeshCoord. - - Parameters - ---------- - other : MeshCoordMetadata - The other MeshCoord metadata participating in the lenient - difference. - - Returns - ------- - A list of different metadata member values. - - """ - - # Perform "strict" difference for location / axis. - def func(field): - left = getattr(self, field) - right = getattr(other, field) - return None if left == right else (left, right) - - # Note that, we use "_members" not "_fields". - values = [func(field) for field in self._members] - # Perform lenient difference of the other parent members. - result = super()._difference_lenient(other) - result.extend(values) - - return result - - @wraps(BaseMetadata.combine, assigned=("__doc__",), updated=()) - @lenient_service - def combine(self, other, lenient=None): - return super().combine(other, lenient=lenient) - - @wraps(BaseMetadata.difference, assigned=("__doc__",), updated=()) - @lenient_service - def difference(self, other, lenient=None): - return super().difference(other, lenient=lenient) - - @wraps(BaseMetadata.equal, assigned=("__doc__",), updated=()) - @lenient_service - def equal(self, other, lenient=None): - return super().equal(other, lenient=lenient) - - -# Add our new optional metadata operations into the 'convenience collections' -# of lenient metadata services. -# TODO: when included in 'iris.common.metadata', install each one directly ? -_op_names_and_service_collections = [ - ("combine", SERVICES_COMBINE), - ("difference", SERVICES_DIFFERENCE), - ("__eq__", SERVICES_EQUAL), - ("equal", SERVICES_EQUAL), -] -_metadata_classes = [ConnectivityMetadata, MeshMetadata, MeshCoordMetadata] -for _cls in _metadata_classes: - for _name, _service_collection in _op_names_and_service_collections: - _method = getattr(_cls, _name) - _service_collection.append(_method) - SERVICES.append(_method) - -del ( - _op_names_and_service_collections, - _metadata_classes, - _cls, - _name, - _service_collection, - _method, -) diff --git a/lib/iris/experimental/ugrid/save.py b/lib/iris/experimental/ugrid/save.py deleted file mode 100644 index ddd7e5333c..0000000000 --- a/lib/iris/experimental/ugrid/save.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. - -"""Extension to Iris' NetCDF saving to allow :class:`~iris.experimental.ugrid.mesh.MeshXY` saving in UGRID format. - -Eventual destination: :mod:`iris.fileformats.netcdf`. - -""" - -from collections.abc import Iterable - -from ...fileformats import netcdf - - -def save_mesh(mesh, filename, netcdf_format="NETCDF4"): - """Save mesh(es) to a netCDF file. - - Parameters - ---------- - mesh : :class:`iris.experimental.ugrid.MeshXY` or iterable - Mesh(es) to save. - filename : str - Name of the netCDF file to create. - netcdf_format : str, default="NETCDF4" - Underlying netCDF file format, one of 'NETCDF4', 'NETCDF4_CLASSIC', - 'NETCDF3_CLASSIC' or 'NETCDF3_64BIT'. Default is 'NETCDF4' format. - - """ - # TODO: integrate with standard saving API when no longer 'experimental'. - - if isinstance(mesh, Iterable): - meshes = mesh - else: - meshes = [mesh] - - # Initialise Manager for saving - with netcdf.Saver(filename, netcdf_format) as sman: - # Iterate through the list. - for mesh in meshes: - # Get suitable dimension names. - mesh_dimensions, _ = sman._get_dim_names(mesh) - - # Create dimensions. - sman._create_cf_dimensions(cube=None, dimension_names=mesh_dimensions) - - # Create the mesh components. - sman._add_mesh(mesh) - - # Add a conventions attribute. - # TODO: add 'UGRID' to conventions, when this is agreed with CF ? - sman.update_global_attributes(Conventions=netcdf.CF_CONVENTIONS_VERSION) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 3247aa1960..556642003a 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -25,8 +25,10 @@ import numpy.ma as ma from iris.fileformats.netcdf import _thread_safe_nc +from iris.mesh.components import Connectivity import iris.util import iris.warnings +from iris.warnings import IrisCfLabelVarWarning, IrisCfMissingVarWarning # # CF parse pattern common to both formula terms and measure CF variables. @@ -889,6 +891,226 @@ def identify(cls, variables, ignore=None, target=None, warn=True): return result +class CFUGridConnectivityVariable(CFVariable): + """A CF_UGRID connectivity variable. + + A CF_UGRID connectivity variable points to an index variable identifying + for every element (edge/face/volume) the indices of its corner nodes. The + connectivity array will thus be a matrix of size n-elements x n-corners. + For the indexing one may use either 0- or 1-based indexing; the convention + used should be specified using a ``start_index`` attribute to the index + variable. + + For face elements: the corner nodes should be specified in anticlockwise + direction as viewed from above. For volume elements: use the + additional attribute ``volume_shape_type`` which points to a flag variable + that specifies for every volume its shape. + + Identified by a CF-netCDF variable attribute equal to any one of the values + in :attr:`~iris.mesh.Connectivity.UGRID_CF_ROLES`. + + .. seealso:: + + The UGRID Conventions, https://ugrid-conventions.github.io/ugrid-conventions/ + + """ + + cf_identity = NotImplemented + cf_identities = Connectivity.UGRID_CF_ROLES + + @classmethod + def identify(cls, variables, ignore=None, target=None, warn=True): + result = {} + ignore, target = cls._identify_common(variables, ignore, target) + + # Identify all CF-UGRID connectivity variables. + for nc_var_name, nc_var in target.items(): + # Check for connectivity variable references, iterating through + # the valid cf roles. + for identity in cls.cf_identities: + nc_var_att = getattr(nc_var, identity, None) + + if nc_var_att is not None: + # UGRID only allows for one of each connectivity cf role. + name = nc_var_att.strip() + if name not in ignore: + if name not in variables: + message = ( + f"Missing CF-UGRID connectivity variable " + f"{name}, referenced by netCDF variable " + f"{nc_var_name}" + ) + if warn: + warnings.warn(message, category=IrisCfMissingVarWarning) + else: + # Restrict to non-string type i.e. not a + # CFLabelVariable. + if not _is_str_dtype(variables[name]): + result[name] = CFUGridConnectivityVariable( + name, variables[name] + ) + else: + message = ( + f"Ignoring variable {name}, identified " + f"as a CF-UGRID connectivity - is a " + f"CF-netCDF label variable." + ) + if warn: + warnings.warn( + message, category=IrisCfLabelVarWarning + ) + + return result + + +class CFUGridAuxiliaryCoordinateVariable(CFVariable): + """A CF-UGRID auxiliary coordinate variable. + + A CF-UGRID auxiliary coordinate variable is a CF-netCDF auxiliary + coordinate variable representing the element (node/edge/face/volume) + locations (latitude, longitude or other spatial coordinates, and optional + elevation or other coordinates). These auxiliary coordinate variables will + have length n-elements. + + For elements other than nodes, these auxiliary coordinate variables may + have in turn a ``bounds`` attribute that specifies the bounding coordinates + of the element (thereby duplicating the data in the ``node_coordinates`` + variables). + + Identified by the CF-netCDF variable attribute + ``node_``/``edge_``/``face_``/``volume_coordinates``. + + .. seealso:: + + The UGRID Conventions, https://ugrid-conventions.github.io/ugrid-conventions/ + + """ + + cf_identity = NotImplemented + cf_identities = [ + "node_coordinates", + "edge_coordinates", + "face_coordinates", + "volume_coordinates", + ] + + @classmethod + def identify(cls, variables, ignore=None, target=None, warn=True): + result = {} + ignore, target = cls._identify_common(variables, ignore, target) + + # Identify any CF-UGRID-relevant auxiliary coordinate variables. + for nc_var_name, nc_var in target.items(): + # Check for UGRID auxiliary coordinate variable references. + for identity in cls.cf_identities: + nc_var_att = getattr(nc_var, identity, None) + + if nc_var_att is not None: + for name in nc_var_att.split(): + if name not in ignore: + if name not in variables: + message = ( + f"Missing CF-netCDF auxiliary coordinate " + f"variable {name}, referenced by netCDF " + f"variable {nc_var_name}" + ) + if warn: + warnings.warn( + message, + category=IrisCfMissingVarWarning, + ) + else: + # Restrict to non-string type i.e. not a + # CFLabelVariable. + if not _is_str_dtype(variables[name]): + result[name] = CFUGridAuxiliaryCoordinateVariable( + name, variables[name] + ) + else: + message = ( + f"Ignoring variable {name}, " + f"identified as a CF-netCDF " + f"auxiliary coordinate - is a " + f"CF-netCDF label variable." + ) + if warn: + warnings.warn( + message, + category=IrisCfLabelVarWarning, + ) + + return result + + +class CFUGridMeshVariable(CFVariable): + """A CF-UGRID mesh variable is a dummy variable for storing topology information as attributes. + + A CF-UGRID mesh variable is a dummy variable for storing topology + information as attributes. The mesh variable has the ``cf_role`` + 'mesh_topology'. + + The UGRID conventions describe define the mesh topology as the + interconnection of various geometrical elements of the mesh. The pure + interconnectivity is independent of georeferencing the individual + geometrical elements, but for the practical applications for which the + UGRID CF extension is defined, coordinate data will always be added. + + Identified by the CF-netCDF variable attribute 'mesh'. + + .. seealso:: + + The UGRID Conventions, https://ugrid-conventions.github.io/ugrid-conventions/ + + """ + + cf_identity = "mesh" + + @classmethod + def identify(cls, variables, ignore=None, target=None, warn=True): + result = {} + ignore, target = cls._identify_common(variables, ignore, target) + + # Identify all CF-UGRID mesh variables. + all_vars = target == variables + for nc_var_name, nc_var in target.items(): + if all_vars: + # SPECIAL BEHAVIOUR FOR MESH VARIABLES. + # We are looking for all mesh variables. Check if THIS variable + # is a mesh using its own attributes. + if getattr(nc_var, "cf_role", "") == "mesh_topology": + result[nc_var_name] = CFUGridMeshVariable(nc_var_name, nc_var) + + # Check for mesh variable references. + nc_var_att = getattr(nc_var, cls.cf_identity, None) + + if nc_var_att is not None: + # UGRID only allows for 1 mesh per variable. + name = nc_var_att.strip() + if name not in ignore: + if name not in variables: + message = ( + f"Missing CF-UGRID mesh variable {name}, " + f"referenced by netCDF variable {nc_var_name}" + ) + if warn: + warnings.warn(message, category=IrisCfMissingVarWarning) + else: + # Restrict to non-string type i.e. not a + # CFLabelVariable. + if not _is_str_dtype(variables[name]): + result[name] = CFUGridMeshVariable(name, variables[name]) + else: + message = ( + f"Ignoring variable {name}, identified as a " + f"CF-UGRID mesh - is a CF-netCDF label " + f"variable." + ) + if warn: + warnings.warn(message, category=IrisCfLabelVarWarning) + + return result + + ################################################################################ class CFGroup(MutableMapping): """Collection of 'NetCDF CF Metadata Conventions variables and netCDF global attributes. @@ -980,12 +1202,30 @@ def non_data_variable_names(self): self.grid_mappings, self.labels, self.cell_measures, + self.connectivities, + self.ugrid_coords, + self.meshes, ) result = set() for variable in non_data_variables: result |= set(variable) return result + @property + def connectivities(self): + """Collection of CF-UGRID connectivity variables.""" + return self._cf_getter(CFUGridConnectivityVariable) + + @property + def ugrid_coords(self): + """Collection of CF-UGRID-relevant auxiliary coordinate variables.""" + return self._cf_getter(CFUGridAuxiliaryCoordinateVariable) + + @property + def meshes(self): + """Collection of CF-UGRID mesh variables.""" + return self._cf_getter(CFUGridMeshVariable) + def keys(self): """Return the names of all the CF-netCDF variables in the group.""" return self._cf_variables.keys() @@ -1054,9 +1294,11 @@ class CFReader: CFGridMappingVariable, CFLabelVariable, CFMeasureVariable, + CFUGridConnectivityVariable, + CFUGridAuxiliaryCoordinateVariable, + CFUGridMeshVariable, ) - # TODO: remove once iris.experimental.ugrid.CFUGridReader is folded in. CFGroup = CFGroup def __init__(self, file_source, warn=False, monotonic=False): @@ -1173,9 +1415,7 @@ def _build_cf_groups(self): """Build the first order relationships between CF-netCDF variables.""" def _build(cf_variable): - # TODO: isinstance(cf_variable, UGridMeshVariable) - # UGridMeshVariable currently in experimental.ugrid - circular import. - is_mesh_var = cf_variable.cf_identity == "mesh" + is_mesh_var = isinstance(cf_variable, CFUGridMeshVariable) ugrid_coord_names = [] ugrid_coords = getattr(self.cf_group, "ugrid_coords", None) if ugrid_coords is not None: diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py index 5276657195..55d0a88b79 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -578,15 +578,14 @@ def load_cubes(file_sources, callback=None, constraints=None): Generator of loaded NetCDF :class:`iris.cube.Cube`. """ - # TODO: rationalise UGRID/mesh handling once experimental.ugrid is folded - # into standard behaviour. # Deferred import to avoid circular imports. - from iris.experimental.ugrid.cf import CFUGridReader - from iris.experimental.ugrid.load import ( + from iris.fileformats.cf import CFReader + from iris.io import run_callback + + from .ugrid_load import ( _build_mesh_coords, _meshes_from_cf, ) - from iris.io import run_callback # Create a low-level data-var filter from the original load constraints, if they are suitable. var_callback = _translate_constraints_to_var_callback(constraints) @@ -599,7 +598,7 @@ def load_cubes(file_sources, callback=None, constraints=None): for file_source in file_sources: # Ingest the file. At present may be a filepath or an open netCDF4.Dataset. - with CFUGridReader(file_source) as cf: + with CFReader(file_source) as cf: meshes = _meshes_from_cf(cf) # Process each CF data variable. @@ -683,8 +682,8 @@ def __init__(self, var_dim_chunksizes=None): :class:`~iris.coords.AncillaryVariable` etc. This can be overridden, if required, by variable-specific settings. - For this purpose, :class:`~iris.experimental.ugrid.mesh.MeshCoord` and - :class:`~iris.experimental.ugrid.mesh.Connectivity` are not + For this purpose, :class:`~iris.mesh.MeshCoord` and + :class:`~iris.mesh.Connectivity` are not :class:`~iris.cube.Cube` components, and chunk control on a :class:`~iris.cube.Cube` data-variable will not affect them. diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 179adaf9cd..cfc69143ae 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -271,7 +271,7 @@ def _setncattr(variable, name, attribute): return variable.setncattr(name, attribute) -# NOTE : this matches :class:`iris.experimental.ugrid.mesh.MeshXY.ELEMENTS`, +# NOTE : this matches :class:`iris.mesh.MeshXY.ELEMENTS`, # but in the preferred order for coord/connectivity variables in the file. MESH_ELEMENTS = ("node", "edge", "face") @@ -766,7 +766,7 @@ def _add_mesh(self, cube_or_mesh): Parameters ---------- - cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.MeshXY` + cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.mesh.MeshXY` The Cube or Mesh being saved to the netCDF file. Returns @@ -941,7 +941,7 @@ def _add_aux_coords(self, cube, cf_var_cube, dimension_names): dimension_names : list Names associated with the dimensions of the cube. """ - from iris.experimental.ugrid.mesh import ( + from iris.mesh.components import ( MeshEdgeCoords, MeshFaceCoords, MeshNodeCoords, @@ -1120,7 +1120,7 @@ def _get_dim_names(self, cube_or_mesh): Parameters ---------- - cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.MeshXY` + cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.mesh.MeshXY` The Cube or Mesh being saved to the netCDF file. Returns @@ -1482,7 +1482,7 @@ def _get_coord_variable_name(self, cube_or_mesh, coord): Parameters ---------- - cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.MeshXY` + cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.mesh.MeshXY` The Cube or Mesh being saved to the netCDF file. coord : :class:`iris.coords._DimensionalMetadata` An instance of a coordinate (or similar), for which a CF-netCDF @@ -1524,7 +1524,7 @@ def _get_coord_variable_name(self, cube_or_mesh, coord): # element-coordinate of the mesh. # Name it for it's first dim, i.e. mesh-dim of its location. - from iris.experimental.ugrid.mesh import Connectivity + from iris.mesh import Connectivity # At present, a location-coord cannot be nameless, as the # MeshXY code relies on guess_coord_axis. @@ -1544,7 +1544,7 @@ def _get_mesh_variable_name(self, mesh): Parameters ---------- - mesh : :class:`iris.experimental.ugrid.mesh.MeshXY` + mesh : :class:`iris.mesh.MeshXY` An instance of a Mesh for which a CF-netCDF variable name is required. @@ -1570,7 +1570,7 @@ def _create_mesh(self, mesh): Parameters ---------- - mesh : :class:`iris.experimental.ugrid.mesh.MeshXY` + mesh : :class:`iris.mesh.MeshXY` The Mesh to be saved to CF-netCDF file. Returns @@ -1660,7 +1660,7 @@ def _create_generic_cf_array_var( Parameters ---------- - cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.MeshXY` + cube_or_mesh : :class:`iris.cube.Cube` or :class:`iris.mesh.MeshXY` The Cube or Mesh being saved to the netCDF file. cube_dim_names : list of str The name of each dimension of the cube. @@ -2796,3 +2796,40 @@ def is_valid_packspec(p): result = sman.delayed_completion() return result + + +def save_mesh(mesh, filename, netcdf_format="NETCDF4"): + """Save mesh(es) to a netCDF file. + + Parameters + ---------- + mesh : :class:`iris.mesh.MeshXY` or iterable + Mesh(es) to save. + filename : str + Name of the netCDF file to create. + netcdf_format : str, default="NETCDF4" + Underlying netCDF file format, one of 'NETCDF4', 'NETCDF4_CLASSIC', + 'NETCDF3_CLASSIC' or 'NETCDF3_64BIT'. Default is 'NETCDF4' format. + + """ + if isinstance(mesh, typing.Iterable): + meshes = mesh + else: + meshes = [mesh] + + # Initialise Manager for saving + with Saver(filename, netcdf_format) as sman: + # Iterate through the list. + for mesh in meshes: + # Get suitable dimension names. + mesh_dimensions, _ = sman._get_dim_names(mesh) + + # Create dimensions. + sman._create_cf_dimensions(cube=None, dimension_names=mesh_dimensions) + + # Create the mesh components. + sman._add_mesh(mesh) + + # Add a conventions attribute. + # TODO: add 'UGRID' to conventions, when this is agreed with CF ? + sman.update_global_attributes(Conventions=CF_CONVENTIONS_VERSION) diff --git a/lib/iris/experimental/ugrid/load.py b/lib/iris/fileformats/netcdf/ugrid_load.py similarity index 70% rename from lib/iris/experimental/ugrid/load.py rename to lib/iris/fileformats/netcdf/ugrid_load.py index e6b3436185..210e112629 100644 --- a/lib/iris/experimental/ugrid/load.py +++ b/lib/iris/fileformats/netcdf/ugrid_load.py @@ -3,12 +3,10 @@ # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -r"""Allow the construction of :class:`~iris.experimental.ugrid.mesh.MeshXY`. +r"""Allow the construction of :class:`~iris.mesh.MeshXY`. -Extensions to Iris' NetCDF loading to allow the construction of -:class:`~iris.experimental.ugrid.mesh.MeshXY` from UGRID data in the file. - -Eventual destination: :mod:`iris.fileformats.netcdf`. +Extension functions for Iris NetCDF loading, to construct +:class:`~iris.mesh.MeshXY` from UGRID data in files. .. seealso:: @@ -17,27 +15,21 @@ """ -from contextlib import contextmanager from itertools import groupby from pathlib import Path -import threading import warnings -from ..._deprecation import warn_deprecated -from ...config import get_logger -from ...coords import AuxCoord -from ...fileformats._nc_load_rules.helpers import get_attr_units, get_names -from ...fileformats.netcdf import loader as nc_loader -from ...io import decode_uri, expand_filespecs -from ...util import guess_coord_axis -from ...warnings import IrisCfWarning, IrisDefaultingWarning, IrisIgnoringWarning -from .cf import ( - CFUGridAuxiliaryCoordinateVariable, - CFUGridConnectivityVariable, - CFUGridMeshVariable, - CFUGridReader, -) -from .mesh import Connectivity, MeshXY +from iris.config import get_logger +from iris.coords import AuxCoord +from iris.io import decode_uri, expand_filespecs +from iris.mesh.components import Connectivity, MeshXY +from iris.util import guess_coord_axis +from iris.warnings import IrisCfWarning, IrisDefaultingWarning, IrisIgnoringWarning + +# NOTE: all imports from iris.fileformats.netcdf must be deferred, to avoid circular +# imports. +# This is needed so that load_mesh/load_meshes can be included in the iris.mesh API. + # Configure the logger. logger = get_logger(__name__, propagate=True, handler=False) @@ -55,81 +47,6 @@ class _WarnComboCfDefaultingIgnoring(_WarnComboCfDefaulting, IrisIgnoringWarning pass -class ParseUGridOnLoad(threading.local): - def __init__(self): - """Thead-safe state to enable UGRID-aware NetCDF loading. - - A flag for dictating whether to use the experimental UGRID-aware - version of Iris NetCDF loading. Object is thread-safe. - - Use via the run-time switch - :const:`~iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD`. - Use :meth:`context` to temporarily activate. - - Notes - ----- - .. deprecated:: 1.10 - Do not use -- due to be removed at next major release : - UGRID loading is now **always** active for files containing a UGRID mesh. - - """ - - def __bool__(self): - return True - - @contextmanager - def context(self): - """Activate UGRID-aware NetCDF loading. - - Use the standard Iris loading API while within the context manager. If - the loaded file(s) include any UGRID content, this will be parsed and - attached to the resultant cube(s) accordingly. - - Use via the run-time switch - :const:`~iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD`. - - For example:: - - with PARSE_UGRID_ON_LOAD.context(): - my_cube_list = iris.load([my_file_path, my_file_path2], - constraint=my_constraint, - callback=my_callback) - - Notes - ----- - .. deprecated:: 1.10 - Do not use -- due to be removed at next major release : - UGRID loading is now **always** active for files containing a UGRID mesh. - - Examples - -------- - Replace usage, for example: - - .. code-block:: python - - with iris.experimental.ugrid.PARSE_UGRID_ON_LOAD.context(): - mesh_cubes = iris.load(path) - - with: - - .. code-block:: python - - mesh_cubes = iris.load(path) - - """ - wmsg = ( - "iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD has been deprecated " - "and will be removed. Please remove all uses : these are no longer needed, " - "as UGRID loading is now applied to any file containing a mesh." - ) - warn_deprecated(wmsg) - yield - - -#: Run-time switch for experimental UGRID-aware NetCDF loading. See :class:`~iris.experimental.ugrid.load.ParseUGridOnLoad`. -PARSE_UGRID_ON_LOAD = ParseUGridOnLoad() - - def _meshes_from_cf(cf_reader): """Mesh from cf, common behaviour for extracting meshes from a CFReader. @@ -148,10 +65,10 @@ def _meshes_from_cf(cf_reader): def load_mesh(uris, var_name=None): - """Load single :class:`~iris.experimental.ugrid.mesh.MeshXY` object from 1/more NetCDF files. + """Load a single :class:`~iris.mesh.MeshXY` object from one or more NetCDF files. Raises an error if more/less than one - :class:`~iris.experimental.ugrid.mesh.MeshXY` is found. + :class:`~iris.mesh.MeshXY` is found. Parameters ---------- @@ -159,12 +76,12 @@ def load_mesh(uris, var_name=None): One or more filenames/URI's. Filenames can include wildcards. Any URI's must support OpenDAP. var_name : str, optional - Only return a :class:`~iris.experimental.ugrid.mesh.MeshXY` if its + Only return a :class:`~iris.mesh.MeshXY` if its var_name matches this value. Returns ------- - :class:`iris.experimental.ugrid.mesh.MeshXY` + :class:`iris.mesh.MeshXY` """ meshes_result = load_meshes(uris, var_name) @@ -177,7 +94,7 @@ def load_mesh(uris, var_name=None): def load_meshes(uris, var_name=None): - r"""Load :class:`~iris.experimental.ugrid.mesh.MeshXY` objects from one or more NetCDF files. + r"""Load :class:`~iris.mesh.MeshXY` objects from one or more NetCDF files. Parameters ---------- @@ -185,7 +102,7 @@ def load_meshes(uris, var_name=None): One or more filenames/URI's. Filenames can include wildcards. Any URI's must support OpenDAP. var_name : str, optional - Only return :class:`~iris.experimental.ugrid.mesh.MeshXY` that have + Only return :class:`~iris.mesh.MeshXY` that have var_names matching this value. Returns @@ -193,15 +110,15 @@ def load_meshes(uris, var_name=None): dict A dictionary mapping each mesh-containing file path/URL in the input ``uris`` to a list of the - :class:`~iris.experimental.ugrid.mesh.MeshXY` returned from each. + :class:`~iris.mesh.MeshXY` returned from each. """ - # TODO: rationalise UGRID/mesh handling once experimental.ugrid is folded - # into standard behaviour. - # No constraints or callbacks supported - these assume they are operating + # NOTE: no constraints or callbacks supported - these assume they are operating # on a Cube. - - from ...fileformats import FORMAT_AGENT + # NOTE: dynamic imports avoid circularity : see note with module imports + from iris.fileformats import FORMAT_AGENT + from iris.fileformats.cf import CFReader + import iris.fileformats.netcdf.loader as nc_loader if isinstance(uris, str): uris = [uris] @@ -237,7 +154,7 @@ def load_meshes(uris, var_name=None): result = {} for source in valid_sources: - with CFUGridReader(source) as cf_reader: + with CFReader(source) as cf_reader: meshes_dict = _meshes_from_cf(cf_reader) meshes = list(meshes_dict.values()) if var_name is not None: @@ -248,23 +165,19 @@ def load_meshes(uris, var_name=None): return result -############ -# Object construction. -# Helper functions, supporting netcdf.load_cubes ONLY, expected to -# altered/moved when pyke is removed. - - def _build_aux_coord(coord_var, file_path): """Construct a :class:`~iris.coords.AuxCoord`. Construct a :class:`~iris.coords.AuxCoord` from a given - :class:`~iris.experimental.ugrid.cf.CFUGridAuxiliaryCoordinateVariable`, + :class:`~iris.fileformats.cf.CFUGridAuxiliaryCoordinateVariable`, and guess its mesh axis. - todo: integrate with standard loading API post-pyke. - """ - # TODO: integrate with standard saving API when no longer 'experimental'. + # NOTE: dynamic imports avoid circularity : see note with module imports + from iris.fileformats._nc_load_rules.helpers import get_attr_units, get_names + from iris.fileformats.cf import CFUGridAuxiliaryCoordinateVariable + from iris.fileformats.netcdf import loader as nc_loader + assert isinstance(coord_var, CFUGridAuxiliaryCoordinateVariable) attributes = {} attr_units = get_attr_units(coord_var, attributes) @@ -310,16 +223,18 @@ def _build_aux_coord(coord_var, file_path): def _build_connectivity(connectivity_var, file_path, element_dims): - """Construct a :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """Construct a :class:`~iris.mesh.Connectivity`. - Construct a :class:`~iris.experimental.ugrid.mesh.Connectivity` from a - given :class:`~iris.experimental.ugrid.cf.CFUGridConnectivityVariable`, + Construct a :class:`~iris.mesh.Connectivity` from a + given :class:`~iris.fileformats.cf.CFUGridConnectivityVariable`, and identify the name of its first dimension. - todo: integrate with standard loading API post-pyke. - """ - # TODO: integrate with standard saving API when no longer 'experimental'. + # NOTE: dynamic imports avoid circularity : see note with module imports + from iris.fileformats._nc_load_rules.helpers import get_attr_units, get_names + from iris.fileformats.cf import CFUGridConnectivityVariable + from iris.fileformats.netcdf import loader as nc_loader + assert isinstance(connectivity_var, CFUGridConnectivityVariable) attributes = {} attr_units = get_attr_units(connectivity_var, attributes) @@ -354,15 +269,17 @@ def _build_connectivity(connectivity_var, file_path, element_dims): def _build_mesh(cf, mesh_var, file_path): - """Construct a :class:`~iris.experimental.ugrid.mesh.MeshXY`. - - Construct a :class:`~iris.experimental.ugrid.mesh.MeshXY` from a given - :class:`~iris.experimental.ugrid.cf.CFUGridMeshVariable`. + """Construct a :class:`~iris.mesh.MeshXY`. - TODO: integrate with standard loading API post-pyke. + Construct a :class:`~iris.mesh.MeshXY` from a given + :class:`~iris.fileformats.cf.CFUGridMeshVariable`. """ - # TODO: integrate with standard saving API when no longer 'experimental'. + # NOTE: dynamic imports avoid circularity : see note with module imports + from iris.fileformats._nc_load_rules.helpers import get_attr_units, get_names + from iris.fileformats.cf import CFUGridMeshVariable + from iris.fileformats.netcdf import loader as nc_loader + assert isinstance(mesh_var, CFUGridMeshVariable) attributes = {} attr_units = get_attr_units(mesh_var, attributes) @@ -489,16 +406,13 @@ def _build_mesh(cf, mesh_var, file_path): def _build_mesh_coords(mesh, cf_var): - """Construct a tuple of :class:`~iris.experimental.ugrid.mesh.MeshCoord`. + """Construct a tuple of :class:`~iris.mesh.MeshCoord`. - Construct a tuple of :class:`~iris.experimental.ugrid.mesh.MeshCoord` using - from a given :class:`~iris.experimental.ugrid.mesh.MeshXY` + Construct a tuple of :class:`~iris.mesh.MeshCoord` using + from a given :class:`~iris.mesh.MeshXY` and :class:`~iris.fileformats.cf.CFVariable`. - TODO: integrate with standard loading API post-pyke. - """ - # TODO: integrate with standard saving API when no longer 'experimental'. # Identify the cube's mesh dimension, for attaching MeshCoords. element_dimensions = { "node": mesh.node_dimension, diff --git a/lib/iris/experimental/ugrid/__init__.py b/lib/iris/mesh/__init__.py similarity index 50% rename from lib/iris/experimental/ugrid/__init__.py rename to lib/iris/mesh/__init__.py index 56c1a73411..9a2c10b7ca 100644 --- a/lib/iris/experimental/ugrid/__init__.py +++ b/lib/iris/mesh/__init__.py @@ -5,32 +5,20 @@ """Infra-structure for unstructured mesh support. -.. deprecated:: 1.10 - - :data:`PARSE_UGRID_ON_LOAD` is due to be removed at next major release. - Please remove all uses of this, which are no longer needed : - UGRID loading is now **always** active for files containing a UGRID mesh. - Based on CF UGRID Conventions (v1.0), https://ugrid-conventions.github.io/ugrid-conventions/. - -.. note:: - - For the docstring of :const:`PARSE_UGRID_ON_LOAD`: see the original - definition at :const:`iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD`. - """ -from ...config import get_logger -from .load import PARSE_UGRID_ON_LOAD, load_mesh, load_meshes -from .mesh import Connectivity, MeshCoord, MeshXY -from .save import save_mesh +from iris.config import get_logger +from iris.fileformats.netcdf.saver import save_mesh +from iris.fileformats.netcdf.ugrid_load import load_mesh, load_meshes + +from .components import Connectivity, MeshCoord, MeshXY from .utils import recombine_submeshes __all__ = [ "Connectivity", "MeshCoord", "MeshXY", - "PARSE_UGRID_ON_LOAD", "load_mesh", "load_meshes", "recombine_submeshes", diff --git a/lib/iris/experimental/ugrid/mesh.py b/lib/iris/mesh/components.py similarity index 94% rename from lib/iris/experimental/ugrid/mesh.py rename to lib/iris/mesh/components.py index 398c240337..a5936388f8 100644 --- a/lib/iris/experimental/ugrid/mesh.py +++ b/lib/iris/mesh/components.py @@ -20,15 +20,16 @@ from dask import array as da import numpy as np -from ... import _lazy_data as _lazy -from ...common import CFVariableMixin, metadata_filter, metadata_manager_factory -from ...common.metadata import BaseMetadata -from ...config import get_logger -from ...coords import AuxCoord, _DimensionalMetadata -from ...exceptions import ConnectivityNotFoundError, CoordinateNotFoundError -from ...util import array_equal, clip_string, guess_coord_axis -from ...warnings import IrisVagueMetadataWarning -from .metadata import ConnectivityMetadata, MeshCoordMetadata, MeshMetadata +from iris.common.metadata import ConnectivityMetadata, MeshCoordMetadata, MeshMetadata + +from .. import _lazy_data as _lazy +from ..common import CFVariableMixin, metadata_filter, metadata_manager_factory +from ..common.metadata import BaseMetadata +from ..config import get_logger +from ..coords import AuxCoord, _DimensionalMetadata +from ..exceptions import ConnectivityNotFoundError, CoordinateNotFoundError +from ..util import array_equal, clip_string, guess_coord_axis +from ..warnings import IrisVagueMetadataWarning # Configure the logger. logger = get_logger(__name__, propagate=True, handler=False) @@ -71,9 +72,9 @@ # MeshXY connectivity manager namedtuples. # -#: Namedtuple for 1D mesh :class:`~iris.experimental.ugrid.mesh.Connectivity` instances. +#: Namedtuple for 1D mesh :class:`~iris.mesh.Connectivity` instances. Mesh1DConnectivities = namedtuple("Mesh1DConnectivities", ["edge_node"]) -#: Namedtuple for 2D mesh :class:`~iris.experimental.ugrid.mesh.Connectivity` instances. +#: Namedtuple for 2D mesh :class:`~iris.mesh.Connectivity` instances. Mesh2DConnectivities = namedtuple( "Mesh2DConnectivities", [ @@ -785,7 +786,7 @@ def from_coords(cls, *coords): .. testsetup:: from iris import load_cube, sample_data_path - from iris.experimental.ugrid import ( + from iris.mesh import ( MeshXY, MeshCoord, ) @@ -1134,7 +1135,7 @@ def _set_dimension_names(self, node, edge, face, reset=False): @property def all_connectivities(self): - """All the :class:`~iris.experimental.ugrid.mesh.Connectivity` instances of the :class:`MeshXY`.""" + """All the :class:`~iris.mesh.Connectivity` instances of the :class:`MeshXY`.""" return self._connectivity_manager.all_members @property @@ -1144,10 +1145,10 @@ def all_coords(self): @property def boundary_node_connectivity(self): - """The *optional* UGRID ``boundary_node_connectivity`` :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """The *optional* UGRID ``boundary_node_connectivity`` :class:`~iris.mesh.Connectivity`. The *optional* UGRID ``boundary_node_connectivity`` - :class:`~iris.experimental.ugrid.mesh.Connectivity` of the + :class:`~iris.mesh.Connectivity` of the :class:`MeshXY`. """ @@ -1173,10 +1174,10 @@ def edge_dimension(self, name): @property def edge_face_connectivity(self): - """The *optional* UGRID ``edge_face_connectivity`` :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """The *optional* UGRID ``edge_face_connectivity`` :class:`~iris.mesh.Connectivity`. The *optional* UGRID ``edge_face_connectivity`` - :class:`~iris.experimental.ugrid.mesh.Connectivity` of the + :class:`~iris.mesh.Connectivity` of the :class:`MeshXY`. """ @@ -1184,10 +1185,10 @@ def edge_face_connectivity(self): @property def edge_node_connectivity(self): - """The UGRID ``edge_node_connectivity`` :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """The UGRID ``edge_node_connectivity`` :class:`~iris.mesh.Connectivity`. The UGRID ``edge_node_connectivity`` - :class:`~iris.experimental.ugrid.mesh.Connectivity` of the + :class:`~iris.mesh.Connectivity` of the :class:`MeshXY`, which is **required** for :attr:`MeshXY.topology_dimension` of ``1``, and *optionally required* for :attr:`MeshXY.topology_dimension` ``>=2``. @@ -1224,10 +1225,10 @@ def face_dimension(self, name): @property def face_edge_connectivity(self): - """The *optional* UGRID ``face_edge_connectivity``:class:`~iris.experimental.ugrid.mesh.Connectivity`. + """The *optional* UGRID ``face_edge_connectivity``:class:`~iris.mesh.Connectivity`. The *optional* UGRID ``face_edge_connectivity`` - :class:`~iris.experimental.ugrid.mesh.Connectivity` of the + :class:`~iris.mesh.Connectivity` of the :class:`MeshXY`. """ @@ -1236,10 +1237,10 @@ def face_edge_connectivity(self): @property def face_face_connectivity(self): - """The *optional* UGRID ``face_face_connectivity`` :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """The *optional* UGRID ``face_face_connectivity`` :class:`~iris.mesh.Connectivity`. The *optional* UGRID ``face_face_connectivity`` - :class:`~iris.experimental.ugrid.mesh.Connectivity` of the + :class:`~iris.mesh.Connectivity` of the :class:`MeshXY`. """ @@ -1247,10 +1248,10 @@ def face_face_connectivity(self): @property def face_node_connectivity(self): - """Return ``face_node_connectivity``:class:`~iris.experimental.ugrid.mesh.Connectivity`. + """Return ``face_node_connectivity``:class:`~iris.mesh.Connectivity`. The UGRID ``face_node_connectivity`` - :class:`~iris.experimental.ugrid.mesh.Connectivity` of the + :class:`~iris.mesh.Connectivity` of the :class:`MeshXY`, which is **required** for :attr:`MeshXY.topology_dimension` of ``2``, and *optionally required* for :attr:`MeshXY.topology_dimension` of ``3``. @@ -1277,13 +1278,13 @@ def node_dimension(self, name): self._metadata_manager.node_dimension = node_dimension def add_connectivities(self, *connectivities): - """Add one or more :class:`~iris.experimental.ugrid.mesh.Connectivity` instances to the :class:`MeshXY`. + """Add one or more :class:`~iris.mesh.Connectivity` instances to the :class:`MeshXY`. Parameters ---------- *connectivities : iterable of object A collection of one or more - :class:`~iris.experimental.ugrid.mesh.Connectivity` instances to + :class:`~iris.mesh.Connectivity` instances to add to the :class:`MeshXY`. """ @@ -1342,10 +1343,10 @@ def connectivities( contains_edge=None, contains_face=None, ): - """Return all :class:`~iris.experimental.ugrid.mesh.Connectivity`. + r"""Return all :class:`~iris.mesh.Connectivity`\s. - Return all :class:`~iris.experimental.ugrid.mesh.Connectivity` - instances from the :class:`MeshXY` that match the provided criteria. + Return all :class:`~iris.mesh.Connectivity` + instances from the :class:`~iris.mesh.MeshXY` which match the provided criteria. Criteria can be either specific properties or other objects with metadata to be matched. @@ -1366,44 +1367,44 @@ def connectivities( * a connectivity or metadata instance equal to that of the desired objects e.g., - :class:`~iris.experimental.ugrid.mesh.Connectivity` or - :class:`~iris.experimental.ugrid.metadata.ConnectivityMetadata`. + :class:`~iris.mesh.Connectivity` or + :class:`~iris.common.metadata.ConnectivityMetadata`. standard_name : str, optional The CF standard name of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``standard_name``. long_name : str, optional An unconstrained description of the - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``long_name``. var_name : str, optional The NetCDF variable name of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``var_name``. attributes : dict, optional A dictionary of attributes desired on the - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``attributes``. cf_role : str, optional The UGRID ``cf_role`` of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. + :class:`~iris.mesh.Connectivity`. contains_node : bool, optional Contains the ``node`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. contains_edge : bool, optional Contains the ``edge`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. contains_face : bool, optional Contains the ``face`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. Returns ------- - list of :class:`~iris.experimental.ugrid.mesh.Connectivity` - A list of :class:`~iris.experimental.ugrid.mesh.Connectivity` + list of :class:`~iris.mesh.Connectivity` + A list of :class:`~iris.mesh.Connectivity` instances from the :class:`MeshXY` that matched the given criteria. """ @@ -1432,9 +1433,9 @@ def connectivity( contains_edge=None, contains_face=None, ): - """Return a single :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """Return a single :class:`~iris.mesh.Connectivity`. - Return a single :class:`~iris.experimental.ugrid.mesh.Connectivity` + Return a single :class:`~iris.mesh.Connectivity` from the :class:`MeshXY` that matches the provided criteria. Criteria can be either specific properties or other objects with @@ -1443,7 +1444,7 @@ def connectivity( .. note:: If the given criteria do not return **precisely one** - :class:`~iris.experimental.ugrid.mesh.Connectivity`, then a + :class:`~iris.mesh.Connectivity`, then a :class:`~iris.exceptions.ConnectivityNotFoundError` is raised. .. seealso:: @@ -1462,44 +1463,44 @@ def connectivity( * a connectivity or metadata instance equal to that of the desired object e.g., - :class:`~iris.experimental.ugrid.mesh.Connectivity` or - :class:`~iris.experimental.ugrid.metadata.ConnectivityMetadata`. + :class:`~iris.mesh.Connectivity` or + :class:`~iris.common.metadata.ConnectivityMetadata`. standard_name : str, optional The CF standard name of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``standard_name``. long_name : str, optional An unconstrained description of the - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``long_name``. var_name : str, optional The NetCDF variable name of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``var_name``. attributes : dict, optional A dictionary of attributes desired on the - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``attributes``. cf_role : str, optional The UGRID ``cf_role`` of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. + :class:`~iris.mesh.Connectivity`. contains_node : bool, optional Contains the ``node`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. contains_edge : bool, optional Contains the ``edge`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. contains_face : bool, optional Contains the ``face`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. Returns ------- - :class:`~iris.experimental.ugrid.mesh.Connectivity` - The :class:`~iris.experimental.ugrid.mesh.Connectivity` from the + :class:`~iris.mesh.Connectivity` + The :class:`~iris.mesh.Connectivity` from the :class:`MeshXY` that matched the given criteria. """ @@ -1605,8 +1606,8 @@ def coords( ): """Return all :class:`~iris.coords.AuxCoord` coordinates from the :class:`MeshXY`. - Return all :class:`~iris.coords.AuxCoord` coordinates from the :class:`MeshXY` that - match the provided criteria. + Return all :class:`~iris.coords.AuxCoord` coordinates from the :class:`MeshXY` + which match the provided criteria. Criteria can be either specific properties or other objects with metadata to be matched. @@ -1677,10 +1678,10 @@ def remove_connectivities( contains_edge=None, contains_face=None, ): - """Remove one or more :class:`~iris.experimental.ugrid.mesh.Connectivity`. + """Remove one or more :class:`~iris.mesh.Connectivity`. - Remove one or more :class:`~iris.experimental.ugrid.mesh.Connectivity` - from the :class:`MeshXY` that match the provided criteria. + Remove one or more :class:`~iris.mesh.Connectivity` + from the :class:`MeshXY` which match the provided criteria. Criteria can be either specific properties or other objects with metadata to be matched. @@ -1697,44 +1698,44 @@ def remove_connectivities( * a connectivity or metadata instance equal to that of the desired objects e.g., - :class:`~iris.experimental.ugrid.mesh.Connectivity` or - :class:`~iris.experimental.ugrid.metadata.ConnectivityMetadata`. + :class:`~iris.mesh.Connectivity` or + :class:`~iris.common.metadata.ConnectivityMetadata`. standard_name : str, optional The CF standard name of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``standard_name``. long_name : str, optional An unconstrained description of the - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``long_name``. var_name : str, optional The NetCDF variable name of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``var_name``. attributes : dict, optional A dictionary of attributes desired on the - :class:`~iris.experimental.ugrid.mesh.Connectivity`. If ``None``, + :class:`~iris.mesh.Connectivity`. If ``None``, does not check for ``attributes``. cf_role : str, optional The UGRID ``cf_role`` of the desired - :class:`~iris.experimental.ugrid.mesh.Connectivity`. + :class:`~iris.mesh.Connectivity`. contains_node : bool, optional Contains the ``node`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. contains_edge : bool, optional Contains the ``edge`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. contains_face : bool, optional Contains the ``face`` element as part of the - :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` + :attr:`~iris.common.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. Returns ------- - list of :class:`~iris.experimental.ugrid.mesh.Connectivity` - A list of :class:`~iris.experimental.ugrid.mesh.Connectivity` + list of :class:`~iris.mesh.Connectivity` + A list of :class:`~iris.mesh.Connectivity` instances removed from the :class:`MeshXY` that matched the given criteria. @@ -1764,7 +1765,7 @@ def remove_coords( """Remove one or more :class:`~iris.coords.AuxCoord` from the :class:`MeshXY`. Remove one or more :class:`~iris.coords.AuxCoord` from the :class:`MeshXY` - that match the provided criteria. + which match the provided criteria. Criteria can be either specific properties or other objects with metadata to be matched. @@ -1852,9 +1853,9 @@ def xml_element(self, doc): # # return the lazy AuxCoord(...), AuxCoord(...) def to_MeshCoord(self, location, axis): - """Generate a :class:`~iris.experimental.ugrid.mesh.MeshCoord`. + """Generate a :class:`~iris.mesh.MeshCoord`. - Generate a :class:`~iris.experimental.ugrid.mesh.MeshCoord` that + Generate a :class:`~iris.mesh.MeshCoord` that references the current :class:`MeshXY`, and passing through the ``location`` and ``axis`` arguments. @@ -1866,25 +1867,25 @@ def to_MeshCoord(self, location, axis): ---------- location : str The ``location`` argument for - :class:`~iris.experimental.ugrid.mesh.MeshCoord` instantiation. + :class:`~iris.mesh.MeshCoord` instantiation. axis : str The ``axis`` argument for - :class:`~iris.experimental.ugrid.mesh.MeshCoord` instantiation. + :class:`~iris.mesh.MeshCoord` instantiation. Returns ------- - :class:`~iris.experimental.ugrid.mesh.MeshCoord` - A :class:`~iris.experimental.ugrid.mesh.MeshCoord` referencing the + :class:`~iris.mesh.mesh.MeshCoord` + A :class:`~iris.mesh.mesh.MeshCoord` referencing the current :class:`MeshXY`. """ return MeshCoord(mesh=self, location=location, axis=axis) def to_MeshCoords(self, location): - r"""Generate a tuple of :class:`~iris.experimental.ugrid.mesh.MeshCoord`. + r"""Generate a tuple of :class:`~iris.mesh.mesh.MeshCoord`. Generate a tuple of - :class:`~iris.experimental.ugrid.mesh.MeshCoord`, each referencing + :class:`~iris.mesh.mesh.MeshCoord`, each referencing the current :class:`MeshXY`, one for each :attr:`AXES` value, passing through the ``location`` argument. @@ -1899,8 +1900,8 @@ def to_MeshCoords(self, location): Returns ------- - tuple of :class:`~iris.experimental.ugrid.mesh.MeshCoord` - Tuple of :class:`~iris.experimental.ugrid.mesh.MeshCoord` + tuple of :class:`~iris.mesh.mesh.MeshCoord` + Tuple of :class:`~iris.mesh.mesh.MeshCoord` referencing the current :class:`MeshXY`. One for each value in :attr:`AXES`, using the value for the ``axis`` argument. @@ -2651,8 +2652,8 @@ def face_node(self): class MeshCoord(AuxCoord): """Geographic coordinate values of data on an unstructured mesh. - A MeshCoord references a `~iris.experimental.ugrid.mesh.MeshXY`. - When contained in a `~iris.cube.Cube` it connects the cube to the MeshXY. + A MeshCoord references a `~iris.mesh.mesh.MeshXY`. + When contained in a `~iris.cube.Cube` it connects the cube to the Mesh. It records (a) which 1-D cube dimension represents the unstructured mesh, and (b) which mesh 'location' the cube data is mapped to -- i.e. is it data on 'face's, 'edge's or 'node's. diff --git a/lib/iris/experimental/ugrid/utils.py b/lib/iris/mesh/utils.py similarity index 99% rename from lib/iris/experimental/ugrid/utils.py rename to lib/iris/mesh/utils.py index b78545c42e..1117c3c7d7 100644 --- a/lib/iris/experimental/ugrid/utils.py +++ b/lib/iris/mesh/utils.py @@ -31,7 +31,7 @@ def recombine_submeshes( Describes the mesh and mesh-location onto which the all the ``submesh-cubes``' data are mapped, and acts as a template for the result. - Must have a :class:`~iris.experimental.ugrid.mesh.MeshXY`. + Must have a :class:`~iris.mesh.MeshXY`. submesh_cubes : iterable of Cube, or Cube Cubes, each with data on a _subset_ of the ``mesh_cube`` datapoints diff --git a/lib/iris/tests/integration/experimental/test_meshcoord_coordsys.py b/lib/iris/tests/integration/mesh/test_meshcoord_coordsys.py similarity index 93% rename from lib/iris/tests/integration/experimental/test_meshcoord_coordsys.py rename to lib/iris/tests/integration/mesh/test_meshcoord_coordsys.py index d9ec782108..9e14b12c9a 100644 --- a/lib/iris/tests/integration/experimental/test_meshcoord_coordsys.py +++ b/lib/iris/tests/integration/mesh/test_meshcoord_coordsys.py @@ -8,7 +8,6 @@ import iris from iris.coord_systems import GeogCS -from iris.experimental.ugrid.load import PARSE_UGRID_ON_LOAD from iris.tests.stock.netcdf import ncgen_from_cdl TEST_CDL = """ @@ -80,8 +79,7 @@ def test_default_mesh_cs(tmp_path, cs_axes): do_x = "x" in cs_axes do_y = "y" in cs_axes make_file(nc_path, node_x_crs=do_x, node_y_crs=do_y) - with PARSE_UGRID_ON_LOAD.context(): - cube = iris.load_cube(nc_path, "node_data") + cube = iris.load_cube(nc_path, "node_data") meshco_x, meshco_y = [cube.coord(mesh_coords=True, axis=ax) for ax in ("x", "y")] # NOTE: at present, none of these load with a coordinate system, # because we don't support the extended grid-mapping syntax. @@ -95,8 +93,7 @@ def test_assigned_mesh_cs(tmp_path): # the corresponding meshcoord reports the same cs. nc_path = tmp_path / "test_temp.nc" make_file(nc_path) - with PARSE_UGRID_ON_LOAD.context(): - cube = iris.load_cube(nc_path, "node_data") + cube = iris.load_cube(nc_path, "node_data") nodeco_x = cube.mesh.coord(location="node", axis="x") meshco_x, meshco_y = [cube.coord(axis=ax) for ax in ("x", "y")] assert nodeco_x.coord_system is None @@ -116,8 +113,7 @@ def test_meshcoord_coordsys_copy(tmp_path): # Check that copying a meshcoord with a coord system works properly. nc_path = tmp_path / "test_temp.nc" make_file(nc_path) - with PARSE_UGRID_ON_LOAD.context(): - cube = iris.load_cube(nc_path, "node_data") + cube = iris.load_cube(nc_path, "node_data") node_coord = cube.mesh.coord(location="node", axis="x") assigned_cs = GeogCS(1.0) node_coord.coord_system = assigned_cs diff --git a/lib/iris/tests/integration/experimental/test_ugrid_save.py b/lib/iris/tests/integration/mesh/test_ugrid_save.py similarity index 100% rename from lib/iris/tests/integration/experimental/test_ugrid_save.py rename to lib/iris/tests/integration/mesh/test_ugrid_save.py diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/README.txt b/lib/iris/tests/integration/mesh/ugrid_conventions_examples/README.txt similarity index 100% rename from lib/iris/tests/integration/experimental/ugrid_conventions_examples/README.txt rename to lib/iris/tests/integration/mesh/ugrid_conventions_examples/README.txt diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl b/lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl similarity index 100% rename from lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl rename to lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl b/lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl similarity index 100% rename from lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl rename to lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl b/lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl similarity index 100% rename from lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl rename to lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl b/lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl similarity index 100% rename from lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl rename to lib/iris/tests/integration/mesh/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl diff --git a/lib/iris/tests/integration/experimental/test_ugrid_load.py b/lib/iris/tests/integration/netcdf/test_ugrid_load.py similarity index 95% rename from lib/iris/tests/integration/experimental/test_ugrid_load.py rename to lib/iris/tests/integration/netcdf/test_ugrid_load.py index 4325532fc6..82098a5d5a 100644 --- a/lib/iris/tests/integration/experimental/test_ugrid_load.py +++ b/lib/iris/tests/integration/netcdf/test_ugrid_load.py @@ -2,12 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Integration tests for NetCDF-UGRID file loading. - -todo: fold these tests into netcdf tests when experimental.ugrid is folded into - standard behaviour. - -""" +"""Integration tests for NetCDF-UGRID file loading.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -18,8 +13,8 @@ import pytest from iris import Constraint, load -from iris.experimental.ugrid.load import load_mesh, load_meshes -from iris.experimental.ugrid.mesh import MeshXY +from iris.fileformats.netcdf.ugrid_load import load_mesh, load_meshes +from iris.mesh import MeshXY from iris.tests.stock.netcdf import ( _file_from_cdl_template as create_file_from_cdl_template, ) @@ -58,7 +53,7 @@ def common_test(self, load_filename, assert_filename): ) self.assertEqual(1, len(cube_list)) cube = cube_list[0] - self.assertCML(cube, ["experimental", "ugrid", assert_filename]) + self.assertCML(cube, ["mesh", assert_filename]) def test_2D_1t_face_half_levels(self): self.common_test( @@ -125,7 +120,7 @@ def test_multiple_phenomena(self): ["NetCDF", "unstructured_grid", "lfric_surface_mean.nc"] ), ) - self.assertCML(cube_list, ("experimental", "ugrid", "surface_mean.cml")) + self.assertCML(cube_list, ("mesh", "surface_mean.cml")) class TestTolerantLoading(XIOSFileMixin): diff --git a/lib/iris/tests/results/experimental/ugrid/2D_1t_face_half_levels.cml b/lib/iris/tests/results/mesh/2D_1t_face_half_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/2D_1t_face_half_levels.cml rename to lib/iris/tests/results/mesh/2D_1t_face_half_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/2D_72t_face_half_levels.cml b/lib/iris/tests/results/mesh/2D_72t_face_half_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/2D_72t_face_half_levels.cml rename to lib/iris/tests/results/mesh/2D_72t_face_half_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/3D_1t_face_full_levels.cml b/lib/iris/tests/results/mesh/3D_1t_face_full_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/3D_1t_face_full_levels.cml rename to lib/iris/tests/results/mesh/3D_1t_face_full_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/3D_1t_face_half_levels.cml b/lib/iris/tests/results/mesh/3D_1t_face_half_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/3D_1t_face_half_levels.cml rename to lib/iris/tests/results/mesh/3D_1t_face_half_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/3D_snow_pseudo_levels.cml b/lib/iris/tests/results/mesh/3D_snow_pseudo_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/3D_snow_pseudo_levels.cml rename to lib/iris/tests/results/mesh/3D_snow_pseudo_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/3D_soil_pseudo_levels.cml b/lib/iris/tests/results/mesh/3D_soil_pseudo_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/3D_soil_pseudo_levels.cml rename to lib/iris/tests/results/mesh/3D_soil_pseudo_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/3D_tile_pseudo_levels.cml b/lib/iris/tests/results/mesh/3D_tile_pseudo_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/3D_tile_pseudo_levels.cml rename to lib/iris/tests/results/mesh/3D_tile_pseudo_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/3D_veg_pseudo_levels.cml b/lib/iris/tests/results/mesh/3D_veg_pseudo_levels.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/3D_veg_pseudo_levels.cml rename to lib/iris/tests/results/mesh/3D_veg_pseudo_levels.cml diff --git a/lib/iris/tests/results/experimental/ugrid/surface_mean.cml b/lib/iris/tests/results/mesh/surface_mean.cml similarity index 100% rename from lib/iris/tests/results/experimental/ugrid/surface_mean.cml rename to lib/iris/tests/results/mesh/surface_mean.cml diff --git a/lib/iris/tests/stock/__init__.py b/lib/iris/tests/stock/__init__.py index e6ef0356a6..31fa7e653d 100644 --- a/lib/iris/tests/stock/__init__.py +++ b/lib/iris/tests/stock/__init__.py @@ -15,6 +15,7 @@ import numpy as np import numpy.ma as ma +from iris import mesh as ugrid from iris.analysis import cartography import iris.aux_factory from iris.coord_systems import GeogCS, RotatedGeogCS @@ -22,7 +23,6 @@ import iris.coords as icoords from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, CellMethod, DimCoord from iris.cube import Cube -from iris.experimental import ugrid from iris.util import mask_cube from ._stock_2d_latlons import ( # noqa diff --git a/lib/iris/tests/stock/mesh.py b/lib/iris/tests/stock/mesh.py index 6333374d6c..3824ba84fc 100644 --- a/lib/iris/tests/stock/mesh.py +++ b/lib/iris/tests/stock/mesh.py @@ -8,7 +8,7 @@ from iris.coords import AuxCoord, DimCoord from iris.cube import Cube -from iris.experimental.ugrid.mesh import Connectivity, MeshCoord, MeshXY +from iris.mesh import Connectivity, MeshCoord, MeshXY # Default creation controls for creating a test MeshXY. # Note: we're not creating any kind of sensible 'normal' mesh here, the numbers diff --git a/lib/iris/tests/stock/netcdf.py b/lib/iris/tests/stock/netcdf.py index c063f3af23..0f5fb0f144 100644 --- a/lib/iris/tests/stock/netcdf.py +++ b/lib/iris/tests/stock/netcdf.py @@ -116,7 +116,7 @@ def _add_standard_data(nc_path, unlimited_dim_size=0): var[:] = data else: # Fill with a plain value. But avoid zeros, so we can simulate - # valid ugrid connectivities even when start_index=1. + # valid mesh connectivities even when start_index=1. with dask.config.set({"array.chunk-size": "2048MiB"}): data = da.ones(shape, dtype=var.dtype) # Do not use zero da.store(data, var) diff --git a/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py b/lib/iris/tests/unit/common/metadata/test_ConnectivityMetadata.py similarity index 99% rename from lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py rename to lib/iris/tests/unit/common/metadata/test_ConnectivityMetadata.py index 91637ad20b..6f3c9f7429 100644 --- a/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_ConnectivityMetadata.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.metadata.ConnectivityMetadata`.""" +"""Unit tests for the :class:`iris.common.metadata.ConnectivityMetadata`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -13,8 +13,7 @@ from unittest.mock import sentinel from iris.common.lenient import _LENIENT, _qualname -from iris.common.metadata import BaseMetadata -from iris.experimental.ugrid.metadata import ConnectivityMetadata +from iris.common.metadata import BaseMetadata, ConnectivityMetadata class Test(tests.IrisTest): diff --git a/lib/iris/tests/unit/experimental/ugrid/metadata/test_MeshCoordMetadata.py b/lib/iris/tests/unit/common/metadata/test_MeshCoordMetadata.py similarity index 99% rename from lib/iris/tests/unit/experimental/ugrid/metadata/test_MeshCoordMetadata.py rename to lib/iris/tests/unit/common/metadata/test_MeshCoordMetadata.py index 0434149674..3bdf261165 100644 --- a/lib/iris/tests/unit/experimental/ugrid/metadata/test_MeshCoordMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_MeshCoordMetadata.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.metadata.MeshCoordMetadata`.""" +"""Unit tests for the :class:`iris.common.metadata.MeshCoordMetadata`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -13,8 +13,7 @@ from unittest.mock import sentinel from iris.common.lenient import _LENIENT, _qualname -from iris.common.metadata import BaseMetadata -from iris.experimental.ugrid.metadata import MeshCoordMetadata +from iris.common.metadata import BaseMetadata, MeshCoordMetadata class Test__identity(tests.IrisTest): diff --git a/lib/iris/tests/unit/experimental/ugrid/metadata/test_MeshMetadata.py b/lib/iris/tests/unit/common/metadata/test_MeshMetadata.py similarity index 99% rename from lib/iris/tests/unit/experimental/ugrid/metadata/test_MeshMetadata.py rename to lib/iris/tests/unit/common/metadata/test_MeshMetadata.py index abbb4c0304..7dae56bbe6 100644 --- a/lib/iris/tests/unit/experimental/ugrid/metadata/test_MeshMetadata.py +++ b/lib/iris/tests/unit/common/metadata/test_MeshMetadata.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.metadata.MeshMetadata`.""" +"""Unit tests for the :class:`iris.common.metadata.MeshMetadata`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -13,8 +13,7 @@ from unittest.mock import sentinel from iris.common.lenient import _LENIENT, _qualname -from iris.common.metadata import BaseMetadata -from iris.experimental.ugrid.metadata import MeshMetadata +from iris.common.metadata import BaseMetadata, MeshMetadata class Test(tests.IrisTest): diff --git a/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py b/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py index e9ec42e04b..1fbf0da084 100644 --- a/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py +++ b/lib/iris/tests/unit/common/metadata/test_metadata_manager_factory.py @@ -17,11 +17,11 @@ AncillaryVariableMetadata, BaseMetadata, CellMeasureMetadata, + ConnectivityMetadata, CoordMetadata, CubeMetadata, metadata_manager_factory, ) -from iris.experimental.ugrid.metadata import ConnectivityMetadata BASES = [ AncillaryVariableMetadata, diff --git a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py index 020f18a358..7d414bfb54 100644 --- a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py @@ -17,11 +17,11 @@ AncillaryVariableMetadata, BaseMetadata, CellMeasureMetadata, + ConnectivityMetadata, CoordMetadata, CubeMetadata, ) from iris.common.mixin import CFVariableMixin, LimitedAttributeDict -from iris.experimental.ugrid.metadata import ConnectivityMetadata class Test__getter(tests.IrisTest): diff --git a/lib/iris/tests/unit/coords/test__DimensionalMetadata.py b/lib/iris/tests/unit/coords/test__DimensionalMetadata.py index 6aaa26e5a9..64246261ca 100644 --- a/lib/iris/tests/unit/coords/test__DimensionalMetadata.py +++ b/lib/iris/tests/unit/coords/test__DimensionalMetadata.py @@ -21,7 +21,7 @@ DimCoord, _DimensionalMetadata, ) -from iris.experimental.ugrid.mesh import Connectivity +from iris.mesh import Connectivity from iris.tests.stock import climatology_3d as cube_with_climatology from iris.tests.stock.mesh import sample_meshcoord diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridGroup.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridGroup.py deleted file mode 100644 index 6db067fe25..0000000000 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridGroup.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.cf.CFUGridGroup` class. - -todo: fold these tests into cf tests when experimental.ugrid is folded into - standard behaviour. - -""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - -from unittest.mock import MagicMock - -from iris.experimental.ugrid.cf import ( - CFUGridAuxiliaryCoordinateVariable, - CFUGridConnectivityVariable, - CFUGridGroup, - CFUGridMeshVariable, -) -from iris.fileformats.cf import CFCoordinateVariable, CFDataVariable - - -class Tests(tests.IrisTest): - def setUp(self): - self.cf_group = CFUGridGroup() - - def test_inherited(self): - coord_var = MagicMock(spec=CFCoordinateVariable, cf_name="coord_var") - self.cf_group[coord_var.cf_name] = coord_var - self.assertEqual(coord_var, self.cf_group.coordinates[coord_var.cf_name]) - - def test_connectivities(self): - conn_var = MagicMock(spec=CFUGridConnectivityVariable, cf_name="conn_var") - self.cf_group[conn_var.cf_name] = conn_var - self.assertEqual(conn_var, self.cf_group.connectivities[conn_var.cf_name]) - - def test_ugrid_coords(self): - coord_var = MagicMock( - spec=CFUGridAuxiliaryCoordinateVariable, cf_name="coord_var" - ) - self.cf_group[coord_var.cf_name] = coord_var - self.assertEqual(coord_var, self.cf_group.ugrid_coords[coord_var.cf_name]) - - def test_meshes(self): - mesh_var = MagicMock(spec=CFUGridMeshVariable, cf_name="mesh_var") - self.cf_group[mesh_var.cf_name] = mesh_var - self.assertEqual(mesh_var, self.cf_group.meshes[mesh_var.cf_name]) - - def test_non_data_names(self): - data_var = MagicMock(spec=CFDataVariable, cf_name="data_var") - coord_var = MagicMock(spec=CFCoordinateVariable, cf_name="coord_var") - conn_var = MagicMock(spec=CFUGridConnectivityVariable, cf_name="conn_var") - ugrid_coord_var = MagicMock( - spec=CFUGridAuxiliaryCoordinateVariable, cf_name="ugrid_coord_var" - ) - mesh_var = MagicMock(spec=CFUGridMeshVariable, cf_name="mesh_var") - mesh_var2 = MagicMock(spec=CFUGridMeshVariable, cf_name="mesh_var2") - duplicate_name_var = MagicMock(spec=CFUGridMeshVariable, cf_name="coord_var") - - for var in ( - data_var, - coord_var, - conn_var, - ugrid_coord_var, - mesh_var, - mesh_var2, - duplicate_name_var, - ): - self.cf_group[var.cf_name] = var - - expected_names = [ - var.cf_name - for var in ( - coord_var, - conn_var, - ugrid_coord_var, - mesh_var, - mesh_var2, - ) - ] - expected = set(expected_names) - self.assertEqual(expected, self.cf_group.non_data_variable_names) diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridReader.py b/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridReader.py deleted file mode 100644 index 14278d3dff..0000000000 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridReader.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.cf.CFUGridGroup` class. - -todo: fold these tests into cf tests when experimental.ugrid is folded into - standard behaviour. - -""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - -from unittest import mock - -from iris.experimental.ugrid.cf import ( - CFUGridAuxiliaryCoordinateVariable, - CFUGridConnectivityVariable, - CFUGridGroup, - CFUGridMeshVariable, - CFUGridReader, -) -from iris.fileformats.cf import CFCoordinateVariable, CFDataVariable -from iris.tests.unit.fileformats.cf.test_CFReader import netcdf_variable - - -def netcdf_ugrid_variable( - name, - dimensions, - dtype, - coordinates=None, -): - ncvar = netcdf_variable( - name=name, dimensions=dimensions, dtype=dtype, coordinates=coordinates - ) - - # Fill in all the extra UGRID attributes to prevent problems with getattr - # and Mock. Any attribute can be replaced in downstream setUp if present. - ugrid_attrs = ( - CFUGridAuxiliaryCoordinateVariable.cf_identities - + CFUGridConnectivityVariable.cf_identities - + [CFUGridMeshVariable.cf_identity] - ) - for attr in ugrid_attrs: - setattr(ncvar, attr, None) - - return ncvar - - -class Test_build_cf_groups(tests.IrisTest): - @classmethod - def setUpClass(cls): - # Replicating syntax from test_CFReader.Test_build_cf_groups__formula_terms. - cls.mesh = netcdf_ugrid_variable("mesh", "", int) - cls.node_x = netcdf_ugrid_variable("node_x", "node", float) - cls.node_y = netcdf_ugrid_variable("node_y", "node", float) - cls.face_x = netcdf_ugrid_variable("face_x", "face", float) - cls.face_y = netcdf_ugrid_variable("face_y", "face", float) - cls.face_nodes = netcdf_ugrid_variable("face_nodes", "face vertex", int) - cls.levels = netcdf_ugrid_variable("levels", "levels", int) - cls.data = netcdf_ugrid_variable( - "data", "levels face", float, coordinates="face_x face_y" - ) - - # Add necessary attributes for mesh recognition. - cls.mesh.cf_role = "mesh_topology" - cls.mesh.node_coordinates = "node_x node_y" - cls.mesh.face_coordinates = "face_x face_y" - cls.mesh.face_node_connectivity = "face_nodes" - cls.face_nodes.cf_role = "face_node_connectivity" - cls.data.mesh = "mesh" - - cls.variables = dict( - mesh=cls.mesh, - node_x=cls.node_x, - node_y=cls.node_y, - face_x=cls.face_x, - face_y=cls.face_y, - face_nodes=cls.face_nodes, - levels=cls.levels, - data=cls.data, - ) - ncattrs = mock.Mock(return_value=[]) - cls.dataset = mock.Mock( - file_format="NetCDF4", variables=cls.variables, ncattrs=ncattrs - ) - - def setUp(self): - # Restrict the CFUGridReader functionality to only performing - # translations and building first level cf-groups for variables. - self.patch("iris.experimental.ugrid.cf.CFUGridReader._reset") - self.patch( - "iris.fileformats.netcdf._thread_safe_nc.DatasetWrapper", - return_value=self.dataset, - ) - cf_reader = CFUGridReader("dummy") - self.cf_group = cf_reader.cf_group - - def test_inherited(self): - for expected_var, collection in ( - [CFCoordinateVariable("levels", self.levels), "coordinates"], - [CFDataVariable("data", self.data), "data_variables"], - ): - expected = {expected_var.cf_name: expected_var} - self.assertDictEqual(expected, getattr(self.cf_group, collection)) - - def test_connectivities(self): - expected_var = CFUGridConnectivityVariable("face_nodes", self.face_nodes) - expected = {expected_var.cf_name: expected_var} - self.assertDictEqual(expected, self.cf_group.connectivities) - - def test_mesh(self): - expected_var = CFUGridMeshVariable("mesh", self.mesh) - expected = {expected_var.cf_name: expected_var} - self.assertDictEqual(expected, self.cf_group.meshes) - - def test_ugrid_coords(self): - names = [f"{loc}_{ax}" for loc in ("node", "face") for ax in ("x", "y")] - expected = { - name: CFUGridAuxiliaryCoordinateVariable(name, getattr(self, name)) - for name in names - } - self.assertDictEqual(expected, self.cf_group.ugrid_coords) - - def test_is_cf_ugrid_group(self): - self.assertIsInstance(self.cf_group, CFUGridGroup) diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/__init__.py b/lib/iris/tests/unit/experimental/ugrid/mesh/__init__.py deleted file mode 100644 index d485782c11..0000000000 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :mod:`iris.experimental.ugrid.mesh` package.""" diff --git a/lib/iris/tests/unit/experimental/ugrid/metadata/__init__.py b/lib/iris/tests/unit/experimental/ugrid/metadata/__init__.py deleted file mode 100644 index a8ad2bc014..0000000000 --- a/lib/iris/tests/unit/experimental/ugrid/metadata/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :mod:`iris.experimental.ugrid.metadata` package.""" diff --git a/lib/iris/tests/unit/experimental/ugrid/load/test_ParseUgridOnLoad.py b/lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py similarity index 80% rename from lib/iris/tests/unit/experimental/ugrid/load/test_ParseUgridOnLoad.py rename to lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py index 0c78fa5880..62961157d8 100644 --- a/lib/iris/tests/unit/experimental/ugrid/load/test_ParseUgridOnLoad.py +++ b/lib/iris/tests/unit/experimental/ugrid/test_ParseUgridOnLoad.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.load.ParseUgridOnLoad` class. +"""Unit tests for the :class:`iris.experimental.ugrid.ParseUgridOnLoad` class. TODO: remove this module when ParseUGridOnLoad itself is removed. @@ -11,7 +11,7 @@ import pytest from iris._deprecation import IrisDeprecation -from iris.experimental.ugrid.load import PARSE_UGRID_ON_LOAD, ParseUGridOnLoad +from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD, ParseUGridOnLoad def test_creation(): diff --git a/lib/iris/tests/unit/experimental/ugrid/utils/__init__.py b/lib/iris/tests/unit/experimental/ugrid/utils/__init__.py deleted file mode 100644 index ea8202f8fb..0000000000 --- a/lib/iris/tests/unit/experimental/ugrid/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the BSD license. -# See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :mod:`iris.experimental.ugrid.utils` package.""" diff --git a/lib/iris/tests/unit/fileformats/cf/test_CFGroup.py b/lib/iris/tests/unit/fileformats/cf/test_CFGroup.py index e1b4b7a7cd..25f64319af 100644 --- a/lib/iris/tests/unit/fileformats/cf/test_CFGroup.py +++ b/lib/iris/tests/unit/fileformats/cf/test_CFGroup.py @@ -11,6 +11,9 @@ CFCoordinateVariable, CFDataVariable, CFGroup, + CFUGridAuxiliaryCoordinateVariable, + CFUGridConnectivityVariable, + CFUGridMeshVariable, ) # Import iris.tests first so that some things can be initialised before @@ -42,3 +45,67 @@ def test_non_data_names(self): expected_names = [var.cf_name for var in (aux_var, coord_var, coord_var2)] expected = set(expected_names) self.assertEqual(expected, self.cf_group.non_data_variable_names) + + +class Ugrid(tests.IrisTest): + """Separate class to test UGRID functionality.""" + + def setUp(self): + self.cf_group = CFGroup() + + def test_inherited(self): + coord_var = MagicMock(spec=CFCoordinateVariable, cf_name="coord_var") + self.cf_group[coord_var.cf_name] = coord_var + self.assertEqual(coord_var, self.cf_group.coordinates[coord_var.cf_name]) + + def test_connectivities(self): + conn_var = MagicMock(spec=CFUGridConnectivityVariable, cf_name="conn_var") + self.cf_group[conn_var.cf_name] = conn_var + self.assertEqual(conn_var, self.cf_group.connectivities[conn_var.cf_name]) + + def test_ugrid_coords(self): + coord_var = MagicMock( + spec=CFUGridAuxiliaryCoordinateVariable, cf_name="coord_var" + ) + self.cf_group[coord_var.cf_name] = coord_var + self.assertEqual(coord_var, self.cf_group.ugrid_coords[coord_var.cf_name]) + + def test_meshes(self): + mesh_var = MagicMock(spec=CFUGridMeshVariable, cf_name="mesh_var") + self.cf_group[mesh_var.cf_name] = mesh_var + self.assertEqual(mesh_var, self.cf_group.meshes[mesh_var.cf_name]) + + def test_non_data_names(self): + data_var = MagicMock(spec=CFDataVariable, cf_name="data_var") + coord_var = MagicMock(spec=CFCoordinateVariable, cf_name="coord_var") + conn_var = MagicMock(spec=CFUGridConnectivityVariable, cf_name="conn_var") + ugrid_coord_var = MagicMock( + spec=CFUGridAuxiliaryCoordinateVariable, cf_name="ugrid_coord_var" + ) + mesh_var = MagicMock(spec=CFUGridMeshVariable, cf_name="mesh_var") + mesh_var2 = MagicMock(spec=CFUGridMeshVariable, cf_name="mesh_var2") + duplicate_name_var = MagicMock(spec=CFUGridMeshVariable, cf_name="coord_var") + + for var in ( + data_var, + coord_var, + conn_var, + ugrid_coord_var, + mesh_var, + mesh_var2, + duplicate_name_var, + ): + self.cf_group[var.cf_name] = var + + expected_names = [ + var.cf_name + for var in ( + coord_var, + conn_var, + ugrid_coord_var, + mesh_var, + mesh_var2, + ) + ] + expected = set(expected_names) + self.assertEqual(expected, self.cf_group.non_data_variable_names) diff --git a/lib/iris/tests/unit/fileformats/cf/test_CFReader.py b/lib/iris/tests/unit/fileformats/cf/test_CFReader.py index 80338ea71e..12c1510413 100644 --- a/lib/iris/tests/unit/fileformats/cf/test_CFReader.py +++ b/lib/iris/tests/unit/fileformats/cf/test_CFReader.py @@ -12,7 +12,15 @@ import numpy as np -from iris.fileformats.cf import CFReader +from iris.fileformats.cf import ( + CFCoordinateVariable, + CFDataVariable, + CFGroup, + CFReader, + CFUGridAuxiliaryCoordinateVariable, + CFUGridConnectivityVariable, + CFUGridMeshVariable, +) def netcdf_variable( @@ -35,6 +43,12 @@ def netcdf_variable( ndim = len(dimensions) else: dimensions = [] + + ugrid_identities = ( + CFUGridAuxiliaryCoordinateVariable.cf_identities + + CFUGridConnectivityVariable.cf_identities + + [CFUGridMeshVariable.cf_identity] + ) ncvar = mock.Mock( name=name, dimensions=dimensions, @@ -49,6 +63,7 @@ def netcdf_variable( grid_mapping=grid_mapping, cell_measures=cell_measures, standard_name=standard_name, + **{name: None for name in ugrid_identities}, ) return ncvar @@ -350,5 +365,84 @@ def test_promoted_auxiliary_ignore(self): self.assertEqual(warn.call_count, 2) +class Test_build_cf_groups__ugrid(tests.IrisTest): + @classmethod + def setUpClass(cls): + # Replicating syntax from test_CFReader.Test_build_cf_groups__formula_terms. + cls.mesh = netcdf_variable("mesh", "", int) + cls.node_x = netcdf_variable("node_x", "node", float) + cls.node_y = netcdf_variable("node_y", "node", float) + cls.face_x = netcdf_variable("face_x", "face", float) + cls.face_y = netcdf_variable("face_y", "face", float) + cls.face_nodes = netcdf_variable("face_nodes", "face vertex", int) + cls.levels = netcdf_variable("levels", "levels", int) + cls.data = netcdf_variable( + "data", "levels face", float, coordinates="face_x face_y" + ) + + # Add necessary attributes for mesh recognition. + cls.mesh.cf_role = "mesh_topology" + cls.mesh.node_coordinates = "node_x node_y" + cls.mesh.face_coordinates = "face_x face_y" + cls.mesh.face_node_connectivity = "face_nodes" + cls.face_nodes.cf_role = "face_node_connectivity" + cls.data.mesh = "mesh" + + cls.variables = dict( + mesh=cls.mesh, + node_x=cls.node_x, + node_y=cls.node_y, + face_x=cls.face_x, + face_y=cls.face_y, + face_nodes=cls.face_nodes, + levels=cls.levels, + data=cls.data, + ) + ncattrs = mock.Mock(return_value=[]) + cls.dataset = mock.Mock( + file_format="NetCDF4", variables=cls.variables, ncattrs=ncattrs + ) + + def setUp(self): + # Restrict the CFReader functionality to only performing + # translations and building first level cf-groups for variables. + self.patch("iris.fileformats.cf.CFReader._reset") + self.patch( + "iris.fileformats.netcdf._thread_safe_nc.DatasetWrapper", + return_value=self.dataset, + ) + cf_reader = CFReader("dummy") + self.cf_group = cf_reader.cf_group + + def test_inherited(self): + for expected_var, collection in ( + [CFCoordinateVariable("levels", self.levels), "coordinates"], + [CFDataVariable("data", self.data), "data_variables"], + ): + expected = {expected_var.cf_name: expected_var} + self.assertDictEqual(expected, getattr(self.cf_group, collection)) + + def test_connectivities(self): + expected_var = CFUGridConnectivityVariable("face_nodes", self.face_nodes) + expected = {expected_var.cf_name: expected_var} + self.assertDictEqual(expected, self.cf_group.connectivities) + + def test_mesh(self): + expected_var = CFUGridMeshVariable("mesh", self.mesh) + expected = {expected_var.cf_name: expected_var} + self.assertDictEqual(expected, self.cf_group.meshes) + + def test_ugrid_coords(self): + names = [f"{loc}_{ax}" for loc in ("node", "face") for ax in ("x", "y")] + expected = { + name: CFUGridAuxiliaryCoordinateVariable(name, getattr(self, name)) + for name in names + } + self.assertDictEqual(expected, self.cf_group.ugrid_coords) + + def test_is_cf_ugrid_group(self): + self.assertIsInstance(self.cf_group, CFGroup) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py b/lib/iris/tests/unit/fileformats/cf/test_CFUGridAuxiliaryCoordinateVariable.py similarity index 93% rename from lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py rename to lib/iris/tests/unit/fileformats/cf/test_CFUGridAuxiliaryCoordinateVariable.py index f283dd22db..d056de4aff 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridAuxiliaryCoordinateVariable.py +++ b/lib/iris/tests/unit/fileformats/cf/test_CFUGridAuxiliaryCoordinateVariable.py @@ -2,12 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.cf.CFUGridAuxiliaryCoordinateVariable` class. - -todo: fold these tests into cf tests when experimental.ugrid is folded into - standard behaviour. - -""" +"""Unit tests for :class:`iris.fileformats.cf.CFUGridAuxiliaryCoordinateVariable`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -19,16 +14,14 @@ import numpy as np import pytest -from iris.experimental.ugrid.cf import CFUGridAuxiliaryCoordinateVariable -from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import ( - netcdf_ugrid_variable, -) +from iris.fileformats.cf import CFUGridAuxiliaryCoordinateVariable +from iris.tests.unit.fileformats.cf.test_CFReader import netcdf_variable import iris.warnings def named_variable(name): # Don't need to worry about dimensions or dtype for these tests. - return netcdf_ugrid_variable(name, "", int) + return netcdf_variable(name, "", int) class TestIdentify(tests.IrisTest): @@ -129,7 +122,7 @@ def test_string_type_ignored(self): ref_source = named_variable("ref_source") setattr(ref_source, self.cf_identities[0], subject_name) vars_all = { - subject_name: netcdf_ugrid_variable(subject_name, "", np.bytes_), + subject_name: netcdf_variable(subject_name, "", np.bytes_), "ref_not_subject": named_variable("ref_not_subject"), "ref_source": ref_source, } @@ -220,7 +213,7 @@ def operation(warn: bool): # String variable warning. warn_regex = r".*is a CF-netCDF label variable.*" - vars_all[subject_name] = netcdf_ugrid_variable(subject_name, "", np.bytes_) + vars_all[subject_name] = netcdf_variable(subject_name, "", np.bytes_) with pytest.warns(iris.warnings.IrisCfLabelVarWarning, match=warn_regex): operation(warn=True) with pytest.warns() as record: diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py b/lib/iris/tests/unit/fileformats/cf/test_CFUGridConnectivityVariable.py similarity index 92% rename from lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py rename to lib/iris/tests/unit/fileformats/cf/test_CFUGridConnectivityVariable.py index d412b8838a..573e6f799f 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridConnectivityVariable.py +++ b/lib/iris/tests/unit/fileformats/cf/test_CFUGridConnectivityVariable.py @@ -2,12 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.cf.CFUGridConnectivityVariable` class. - -todo: fold these tests into cf tests when experimental.ugrid is folded into - standard behaviour. - -""" +"""Unit tests for :class:`iris.fileformats.cf.CFUGridConnectivityVariable`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -19,17 +14,15 @@ import numpy as np import pytest -from iris.experimental.ugrid.cf import CFUGridConnectivityVariable -from iris.experimental.ugrid.mesh import Connectivity -from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import ( - netcdf_ugrid_variable, -) +from iris.fileformats.cf import CFUGridConnectivityVariable +from iris.mesh import Connectivity +from iris.tests.unit.fileformats.cf.test_CFReader import netcdf_variable import iris.warnings def named_variable(name): # Don't need to worry about dimensions or dtype for these tests. - return netcdf_ugrid_variable(name, "", int) + return netcdf_variable(name, "", int) class TestIdentify(tests.IrisTest): @@ -118,7 +111,7 @@ def test_string_type_ignored(self): ref_source = named_variable("ref_source") setattr(ref_source, Connectivity.UGRID_CF_ROLES[0], subject_name) vars_all = { - subject_name: netcdf_ugrid_variable(subject_name, "", np.bytes_), + subject_name: netcdf_variable(subject_name, "", np.bytes_), "ref_not_subject": named_variable("ref_not_subject"), "ref_source": ref_source, } @@ -203,7 +196,7 @@ def operation(warn: bool): # String variable warning. warn_regex = r".*is a CF-netCDF label variable.*" - vars_all[subject_name] = netcdf_ugrid_variable(subject_name, "", np.bytes_) + vars_all[subject_name] = netcdf_variable(subject_name, "", np.bytes_) with pytest.warns(iris.warnings.IrisCfLabelVarWarning, match=warn_regex): operation(warn=True) with pytest.warns() as record: diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py b/lib/iris/tests/unit/fileformats/cf/test_CFUGridMeshVariable.py similarity index 94% rename from lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py rename to lib/iris/tests/unit/fileformats/cf/test_CFUGridMeshVariable.py index 32c96cacbc..5205c6a018 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/test_CFUGridMeshVariable.py +++ b/lib/iris/tests/unit/fileformats/cf/test_CFUGridMeshVariable.py @@ -2,12 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.cf.CFUGridMeshVariable` class. - -todo: fold these tests into cf tests when experimental.ugrid is folded into - standard behaviour. - -""" +"""Unit tests for :class:`iris.fileformats.cf.CFUGridMeshVariable`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -19,16 +14,14 @@ import numpy as np import pytest -from iris.experimental.ugrid.cf import CFUGridMeshVariable -from iris.tests.unit.experimental.ugrid.cf.test_CFUGridReader import ( - netcdf_ugrid_variable, -) +from iris.fileformats.cf import CFUGridMeshVariable +from iris.tests.unit.fileformats.cf.test_CFReader import netcdf_variable import iris.warnings def named_variable(name): # Don't need to worry about dimensions or dtype for these tests. - return netcdf_ugrid_variable(name, "", int) + return netcdf_variable(name, "", int) class TestIdentify(tests.IrisTest): @@ -165,7 +158,7 @@ def test_string_type_ignored(self): ref_source = named_variable("ref_source") setattr(ref_source, self.cf_identity, subject_name) vars_all = { - subject_name: netcdf_ugrid_variable(subject_name, "", np.bytes_), + subject_name: netcdf_variable(subject_name, "", np.bytes_), "ref_not_subject": named_variable("ref_not_subject"), "ref_source": ref_source, } @@ -250,7 +243,7 @@ def operation(warn: bool): # String variable warning. warn_regex = r".*is a CF-netCDF label variable.*" - vars_all[subject_name] = netcdf_ugrid_variable(subject_name, "", np.bytes_) + vars_all[subject_name] = netcdf_variable(subject_name, "", np.bytes_) with pytest.warns(iris.warnings.IrisCfLabelVarWarning, match=warn_regex): operation(warn=True) with pytest.warns() as record: diff --git a/lib/iris/tests/unit/fileformats/netcdf/loader/test_load_cubes.py b/lib/iris/tests/unit/fileformats/netcdf/loader/test_load_cubes.py index 571a749bf0..09ee679adf 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/loader/test_load_cubes.py +++ b/lib/iris/tests/unit/fileformats/netcdf/loader/test_load_cubes.py @@ -21,9 +21,9 @@ import numpy as np from iris.coords import AncillaryVariable, CellMeasure -from iris.experimental.ugrid.mesh import MeshCoord from iris.fileformats.netcdf import logger from iris.fileformats.netcdf.loader import load_cubes +from iris.mesh import MeshCoord from iris.tests.stock.netcdf import ncgen_from_cdl diff --git a/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/__init__.py b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/__init__.py new file mode 100644 index 0000000000..993d106ba3 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/__init__.py @@ -0,0 +1,5 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +"""Unit tests for the :mod:`iris.fileformats.netcdf.ugrid_load` package.""" diff --git a/lib/iris/tests/unit/experimental/ugrid/load/test_load_mesh.py b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_load_mesh.py similarity index 86% rename from lib/iris/tests/unit/experimental/ugrid/load/test_load_mesh.py rename to lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_load_mesh.py index 6d1bbe995c..0e618c7d55 100644 --- a/lib/iris/tests/unit/experimental/ugrid/load/test_load_mesh.py +++ b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_load_mesh.py @@ -2,20 +2,21 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :func:`iris.experimental.ugrid.load.load_mesh` function.""" +"""Unit tests for the :func:`iris.mesh.load_mesh` function.""" # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests # isort:skip -from iris.experimental.ugrid.load import load_mesh +from iris.fileformats.netcdf.ugrid_load import load_mesh class Tests(tests.IrisTest): # All 'real' tests have been done for load_meshes(). Here we just check # that load_mesh() works with load_meshes() correctly, using mocking. def setUp(self): - self.load_meshes_mock = self.patch("iris.experimental.ugrid.load.load_meshes") + tgt = "iris.fileformats.netcdf.ugrid_load.load_meshes" + self.load_meshes_mock = self.patch(tgt) # The expected return from load_meshes - a dict of files, each with # a list of meshes. self.load_meshes_mock.return_value = {"file": ["mesh"]} diff --git a/lib/iris/tests/unit/experimental/ugrid/load/test_load_meshes.py b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_load_meshes.py similarity index 97% rename from lib/iris/tests/unit/experimental/ugrid/load/test_load_meshes.py rename to lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_load_meshes.py index da7cf9b649..424c321098 100644 --- a/lib/iris/tests/unit/experimental/ugrid/load/test_load_meshes.py +++ b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_load_meshes.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :func:`iris.experimental.ugrid.load.load_meshes` function.""" +"""Unit tests for the :func:`iris.mesh.load_meshes` function.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -13,7 +13,7 @@ import tempfile from uuid import uuid4 -from iris.experimental.ugrid.load import load_meshes, logger +from iris.fileformats.netcdf.ugrid_load import load_meshes, logger from iris.tests.stock.netcdf import ncgen_from_cdl diff --git a/lib/iris/tests/unit/experimental/ugrid/load/test_meshload_checks.py b/lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_meshload_checks.py similarity index 100% rename from lib/iris/tests/unit/experimental/ugrid/load/test_meshload_checks.py rename to lib/iris/tests/unit/fileformats/netcdf/loader/ugrid_load/test_meshload_checks.py diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__ugrid.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__ugrid.py index e86ecf9a52..7508376840 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__ugrid.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver__ugrid.py @@ -22,9 +22,8 @@ from iris import save from iris.coords import AuxCoord from iris.cube import Cube, CubeList -from iris.experimental.ugrid.mesh import Connectivity, MeshXY -from iris.experimental.ugrid.save import save_mesh from iris.fileformats.netcdf import _thread_safe_nc +from iris.mesh import Connectivity, MeshXY, save_mesh from iris.tests.stock import realistic_4d XY_LOCS = ("x", "y") @@ -196,7 +195,7 @@ def make_cube(mesh=None, location="face", **kwargs): Parameters ---------- - mesh : :class:`iris.experimental.ugrid.mesh.MeshXY` or None, optional + mesh : :class:`iris.mesh.MeshXY` or None, optional If None, use 'default_mesh()' location : str, optional, default="face" Which mesh element to map the cube to. diff --git a/lib/iris/tests/unit/mesh/__init__.py b/lib/iris/tests/unit/mesh/__init__.py new file mode 100644 index 0000000000..1305bda078 --- /dev/null +++ b/lib/iris/tests/unit/mesh/__init__.py @@ -0,0 +1,7 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +"""Unit tests for the :mod:`iris.mesh` package.""" + +from __future__ import annotations diff --git a/lib/iris/tests/unit/experimental/ugrid/cf/__init__.py b/lib/iris/tests/unit/mesh/components/__init__.py similarity index 71% rename from lib/iris/tests/unit/experimental/ugrid/cf/__init__.py rename to lib/iris/tests/unit/mesh/components/__init__.py index 19507555c7..cc0effb1f6 100644 --- a/lib/iris/tests/unit/experimental/ugrid/cf/__init__.py +++ b/lib/iris/tests/unit/mesh/components/__init__.py @@ -2,4 +2,4 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :mod:`iris.experimental.ugrid.cf` package.""" +"""Unit tests for the :mod:`iris.mesh.components` package.""" diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py b/lib/iris/tests/unit/mesh/components/test_Connectivity.py similarity index 98% rename from lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py rename to lib/iris/tests/unit/mesh/components/test_Connectivity.py index b84b32cf41..de8d7de3d7 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py +++ b/lib/iris/tests/unit/mesh/components/test_Connectivity.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.mesh.Connectivity` class.""" +"""Unit tests for the :class:`iris.mesh.Connectivity` class.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -16,7 +16,7 @@ from packaging import version from iris._lazy_data import as_lazy_data, is_lazy_data -from iris.experimental.ugrid.mesh import Connectivity +from iris.mesh import Connectivity class TestStandard(tests.IrisTest): diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py b/lib/iris/tests/unit/mesh/components/test_MeshCoord.py similarity index 99% rename from lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py rename to lib/iris/tests/unit/mesh/components/test_MeshCoord.py index 3f81085fa9..0acec1985d 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshCoord.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`iris.experimental.ugrid.mesh.MeshCoord`.""" +"""Unit tests for the :class:`iris.mesh.MeshCoord`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -21,7 +21,7 @@ from iris.common.metadata import BaseMetadata, CoordMetadata from iris.coords import AuxCoord, Coord from iris.cube import Cube -from iris.experimental.ugrid.mesh import Connectivity, MeshCoord, MeshXY +from iris.mesh import Connectivity, MeshCoord, MeshXY import iris.tests.stock.mesh from iris.tests.stock.mesh import sample_mesh, sample_meshcoord from iris.warnings import IrisVagueMetadataWarning diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py b/lib/iris/tests/unit/mesh/components/test_MeshXY.py similarity index 92% rename from lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py rename to lib/iris/tests/unit/mesh/components/test_MeshXY.py index a47d093af0..c1977633e2 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshXY.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :class:`mesh` class.""" +"""Unit tests for the :class:`iris.mesh.MeshXY` class.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -10,10 +10,11 @@ import numpy as np +from iris.common.metadata import MeshMetadata from iris.coords import AuxCoord from iris.exceptions import ConnectivityNotFoundError, CoordinateNotFoundError -from iris.experimental.ugrid import mesh, metadata -from iris.experimental.ugrid.mesh import logger +from iris.mesh import components +from iris.mesh.components import logger class TestMeshCommon(tests.IrisTest): @@ -40,22 +41,28 @@ def setUpClass(cls): cls.FACE_LON = AuxCoord([0.5], standard_name="longitude", var_name="face_lon") cls.FACE_LAT = AuxCoord([0.5], standard_name="latitude", var_name="face_lat") - cls.EDGE_NODE = mesh.Connectivity( + cls.EDGE_NODE = components.Connectivity( [[0, 1], [1, 2], [2, 0]], cf_role="edge_node_connectivity", long_name="long_name", var_name="var_name", attributes={"test": 1}, ) - cls.FACE_NODE = mesh.Connectivity([[0, 1, 2]], cf_role="face_node_connectivity") - cls.FACE_EDGE = mesh.Connectivity([[0, 1, 2]], cf_role="face_edge_connectivity") + cls.FACE_NODE = components.Connectivity( + [[0, 1, 2]], cf_role="face_node_connectivity" + ) + cls.FACE_EDGE = components.Connectivity( + [[0, 1, 2]], cf_role="face_edge_connectivity" + ) # (Actually meaningless:) - cls.FACE_FACE = mesh.Connectivity([[0, 0, 0]], cf_role="face_face_connectivity") + cls.FACE_FACE = components.Connectivity( + [[0, 0, 0]], cf_role="face_face_connectivity" + ) # (Actually meaningless:) - cls.EDGE_FACE = mesh.Connectivity( + cls.EDGE_FACE = components.Connectivity( [[0, 0], [0, 0], [0, 0]], cf_role="edge_face_connectivity" ) - cls.BOUNDARY_NODE = mesh.Connectivity( + cls.BOUNDARY_NODE = components.Connectivity( [[0, 1], [1, 2], [2, 0]], cf_role="boundary_node_connectivity" ) @@ -78,12 +85,12 @@ def setUpClass(cls): "edge_dimension": "EdgeDim", "edge_coords_and_axes": ((cls.EDGE_LON, "x"), (cls.EDGE_LAT, "y")), } - cls.mesh = mesh.MeshXY(**cls.kwargs) + cls.mesh = components.MeshXY(**cls.kwargs) def test__metadata_manager(self): self.assertEqual( self.mesh._metadata_manager.cls.__name__, - metadata.MeshMetadata.__name__, + MeshMetadata.__name__, ) def test___getstate__(self): @@ -127,13 +134,13 @@ def test___eq__(self): # The dimension names do not participate in equality. equivalent_kwargs = self.kwargs.copy() equivalent_kwargs["node_dimension"] = "something_else" - equivalent = mesh.MeshXY(**equivalent_kwargs) + equivalent = components.MeshXY(**equivalent_kwargs) self.assertEqual(equivalent, self.mesh) def test_different(self): different_kwargs = self.kwargs.copy() different_kwargs["long_name"] = "new_name" - different = mesh.MeshXY(**different_kwargs) + different = components.MeshXY(**different_kwargs) self.assertNotEqual(different, self.mesh) different_kwargs = self.kwargs.copy() @@ -141,22 +148,22 @@ def test_different(self): new_lat = ncaa[1][0].copy(points=ncaa[1][0].points + 1) new_ncaa = (ncaa[0], (new_lat, "y")) different_kwargs["node_coords_and_axes"] = new_ncaa - different = mesh.MeshXY(**different_kwargs) + different = components.MeshXY(**different_kwargs) self.assertNotEqual(different, self.mesh) different_kwargs = self.kwargs.copy() conns = self.kwargs["connectivities"] new_conn = conns[0].copy(conns[0].indices + 1) different_kwargs["connectivities"] = new_conn - different = mesh.MeshXY(**different_kwargs) + different = components.MeshXY(**different_kwargs) self.assertNotEqual(different, self.mesh) def test_all_connectivities(self): - expected = mesh.Mesh1DConnectivities(self.EDGE_NODE) + expected = components.Mesh1DConnectivities(self.EDGE_NODE) self.assertEqual(expected, self.mesh.all_connectivities) def test_all_coords(self): - expected = mesh.Mesh1DCoords( + expected = components.Mesh1DCoords( self.NODE_LON, self.NODE_LAT, self.EDGE_LON, self.EDGE_LAT ) self.assertEqual(expected, self.mesh.all_coords) @@ -181,7 +188,9 @@ def test_connectivities(self): {"cf_role": "edge_node_connectivity"}, ) - fake_connectivity = tests.mock.Mock(__class__=mesh.Connectivity, cf_role="fake") + fake_connectivity = tests.mock.Mock( + __class__=components.Connectivity, cf_role="fake" + ) negative_kwargs = ( {"item": fake_connectivity}, {"item": "foo"}, @@ -295,7 +304,7 @@ def test_edge_dimension(self): self.assertEqual(self.kwargs["edge_dimension"], self.mesh.edge_dimension) def test_edge_coords(self): - expected = mesh.MeshEdgeCoords(self.EDGE_LON, self.EDGE_LAT) + expected = components.MeshEdgeCoords(self.EDGE_LON, self.EDGE_LAT) self.assertEqual(expected, self.mesh.edge_coords) def test_edge_face(self): @@ -325,7 +334,7 @@ def test_face_node(self): _ = self.mesh.face_node_connectivity def test_node_coords(self): - expected = mesh.MeshNodeCoords(self.NODE_LON, self.NODE_LAT) + expected = components.MeshNodeCoords(self.NODE_LON, self.NODE_LAT) self.assertEqual(expected, self.mesh.node_coords) def test_node_dimension(self): @@ -360,7 +369,7 @@ def setUpClass(cls): (cls.FACE_LON, "x"), (cls.FACE_LAT, "y"), ) - cls.mesh = mesh.MeshXY(**cls.kwargs) + cls.mesh = components.MeshXY(**cls.kwargs) def test___repr__(self): expected = "" @@ -417,7 +426,7 @@ def test___str__(self): def test___str__noedgecoords(self): mesh_kwargs = self.kwargs.copy() del mesh_kwargs["edge_coords_and_axes"] - alt_mesh = mesh.MeshXY(**mesh_kwargs) + alt_mesh = components.MeshXY(**mesh_kwargs) expected = [ "MeshXY : 'my_topology_mesh'", " topology_dimension: 2", @@ -462,7 +471,7 @@ def test___str__noedgecoords(self): self.assertEqual(expected, str(alt_mesh).split("\n")) def test_all_connectivities(self): - expected = mesh.Mesh2DConnectivities( + expected = components.Mesh2DConnectivities( self.FACE_NODE, self.EDGE_NODE, self.FACE_EDGE, @@ -473,7 +482,7 @@ def test_all_connectivities(self): self.assertEqual(expected, self.mesh.all_connectivities) def test_all_coords(self): - expected = mesh.Mesh2DCoords( + expected = components.Mesh2DCoords( self.NODE_LON, self.NODE_LAT, self.EDGE_LON, @@ -583,7 +592,7 @@ def test_edge_face(self): self.assertEqual(self.EDGE_FACE, self.mesh.edge_face_connectivity) def test_face_coords(self): - expected = mesh.MeshFaceCoords(self.FACE_LON, self.FACE_LAT) + expected = components.MeshFaceCoords(self.FACE_LON, self.FACE_LAT) self.assertEqual(expected, self.mesh.face_coords) def test_face_dimension(self): @@ -624,7 +633,7 @@ def setUp(self): (self.EDGE_LAT, "y"), ), } - self.mesh = mesh.MeshXY(**self.kwargs) + self.mesh = components.MeshXY(**self.kwargs) def test___repr__basic(self): expected = "" @@ -667,7 +676,7 @@ def test___str__units_stdname(self): mesh_kwargs = self.kwargs.copy() mesh_kwargs["standard_name"] = "height" # Odd choice ! mesh_kwargs["units"] = "m" - alt_mesh = mesh.MeshXY(**mesh_kwargs) + alt_mesh = components.MeshXY(**mesh_kwargs) result = str(alt_mesh) # We expect these to appear at the end. expected = "\n".join( @@ -690,7 +699,7 @@ class TestOperations1D(TestMeshCommon): # Tests that cannot reuse an existing MeshXY instance, instead need a new # one each time. def setUp(self): - self.mesh = mesh.MeshXY( + self.mesh = components.MeshXY( topology_dimension=1, node_coords_and_axes=((self.NODE_LON, "x"), (self.NODE_LAT, "y")), connectivities=self.EDGE_NODE, @@ -740,7 +749,7 @@ def test_add_connectivities(self): edge_node = self.new_connectivity(self.EDGE_NODE, new_len) self.mesh.add_connectivities(edge_node) self.assertEqual( - mesh.Mesh1DConnectivities(edge_node), + components.Mesh1DConnectivities(edge_node), self.mesh.all_connectivities, ) @@ -771,7 +780,7 @@ def test_add_coords(self): edge_kwargs = {"edge_x": self.EDGE_LON, "edge_y": self.EDGE_LAT} self.mesh.add_coords(**edge_kwargs) self.assertEqual( - mesh.MeshEdgeCoords(**edge_kwargs), + components.MeshEdgeCoords(**edge_kwargs), self.mesh.edge_coords, ) @@ -788,11 +797,11 @@ def test_add_coords(self): } self.mesh.add_coords(**node_kwargs, **edge_kwargs) self.assertEqual( - mesh.MeshNodeCoords(**node_kwargs), + components.MeshNodeCoords(**node_kwargs), self.mesh.node_coords, ) self.assertEqual( - mesh.MeshEdgeCoords(**edge_kwargs), + components.MeshEdgeCoords(**edge_kwargs), self.mesh.edge_coords, ) @@ -833,17 +842,17 @@ def test_add_coords_invalid(self): def test_add_coords_single(self): # ADD coord. edge_x = self.EDGE_LON - expected = mesh.MeshEdgeCoords(edge_x=edge_x, edge_y=None) + expected = components.MeshEdgeCoords(edge_x=edge_x, edge_y=None) self.mesh.add_coords(edge_x=edge_x) self.assertEqual(expected, self.mesh.edge_coords) # REPLACE coords. node_x = self.new_coord(self.NODE_LON) edge_x = self.new_coord(self.EDGE_LON) - expected_nodes = mesh.MeshNodeCoords( + expected_nodes = components.MeshNodeCoords( node_x=node_x, node_y=self.mesh.node_coords.node_y ) - expected_edges = mesh.MeshEdgeCoords(edge_x=edge_x, edge_y=None) + expected_edges = components.MeshEdgeCoords(edge_x=edge_x, edge_y=None) self.mesh.add_coords(node_x=node_x, edge_x=edge_x) self.assertEqual(expected_nodes, self.mesh.node_coords) self.assertEqual(expected_edges, self.mesh.edge_coords) @@ -867,14 +876,14 @@ def test_add_coords_single_face(self): def test_dimension_names(self): # Test defaults. - default = mesh.Mesh1DNames("Mesh1d_node", "Mesh1d_edge") + default = components.Mesh1DNames("Mesh1d_node", "Mesh1d_edge") self.assertEqual(default, self.mesh.dimension_names()) log_regex = r"Not setting face_dimension.*" with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): self.mesh.dimension_names("foo", "bar", "baz") self.assertEqual( - mesh.Mesh1DNames("foo", "bar"), + components.Mesh1DNames("foo", "bar"), self.mesh.dimension_names(), ) @@ -918,7 +927,9 @@ def test_remove_connectivities(self): {"contains_edge": True, "contains_node": True}, ) - fake_connectivity = tests.mock.Mock(__class__=mesh.Connectivity, cf_role="fake") + fake_connectivity = tests.mock.Mock( + __class__=components.Connectivity, cf_role="fake" + ) negative_kwargs = ( {"item": fake_connectivity}, {"item": "foo"}, @@ -995,7 +1006,7 @@ def test_to_MeshCoord(self): location = "node" axis = "x" result = self.mesh.to_MeshCoord(location, axis) - self.assertIsInstance(result, mesh.MeshCoord) + self.assertIsInstance(result, components.MeshCoord) self.assertEqual(location, result.location) self.assertEqual(axis, result.axis) @@ -1012,7 +1023,7 @@ def test_to_MeshCoords(self): self.assertEqual(len(self.mesh.AXES), len(result)) for ix, axis in enumerate(self.mesh.AXES): coord = result[ix] - self.assertIsInstance(coord, mesh.MeshCoord) + self.assertIsInstance(coord, components.MeshCoord) self.assertEqual(location, coord.location) self.assertEqual(axis, coord.axis) @@ -1024,7 +1035,7 @@ def test_to_MeshCoords_face(self): class TestOperations2D(TestOperations1D): # Additional/specialised tests for topology_dimension=2. def setUp(self): - self.mesh = mesh.MeshXY( + self.mesh = components.MeshXY( topology_dimension=2, node_coords_and_axes=((self.NODE_LON, "x"), (self.NODE_LAT, "y")), connectivities=(self.FACE_NODE), @@ -1039,7 +1050,7 @@ def test_add_connectivities(self): "edge_face": self.EDGE_FACE, "boundary_node": self.BOUNDARY_NODE, } - expected = mesh.Mesh2DConnectivities( + expected = components.Mesh2DConnectivities( face_node=self.mesh.face_node_connectivity, **kwargs ) self.mesh.add_connectivities(*kwargs.values()) @@ -1053,7 +1064,7 @@ def test_add_connectivities(self): kwargs = {k: self.new_connectivity(v, new_len) for k, v in kwargs.items()} self.mesh.add_connectivities(*kwargs.values()) self.assertEqual( - mesh.Mesh2DConnectivities(**kwargs), + components.Mesh2DConnectivities(**kwargs), self.mesh.all_connectivities, ) @@ -1081,7 +1092,7 @@ def test_add_connectivities_inconsistent(self): ) def test_add_connectivities_invalid(self): - fake_cf_role = tests.mock.Mock(__class__=mesh.Connectivity, cf_role="foo") + fake_cf_role = tests.mock.Mock(__class__=components.Connectivity, cf_role="foo") log_regex = r"Not adding connectivity.*" with self.assertLogs(logger, level="DEBUG", msg_regex=log_regex): self.mesh.add_connectivities(fake_cf_role) @@ -1091,7 +1102,7 @@ def test_add_coords_face(self): kwargs = {"face_x": self.FACE_LON, "face_y": self.FACE_LAT} self.mesh.add_coords(**kwargs) self.assertEqual( - mesh.MeshFaceCoords(**kwargs), + components.MeshFaceCoords(**kwargs), self.mesh.face_coords, ) @@ -1104,20 +1115,20 @@ def test_add_coords_face(self): } self.mesh.add_coords(**kwargs) self.assertEqual( - mesh.MeshFaceCoords(**kwargs), + components.MeshFaceCoords(**kwargs), self.mesh.face_coords, ) def test_add_coords_single_face(self): # ADD coord. face_x = self.FACE_LON - expected = mesh.MeshFaceCoords(face_x=face_x, face_y=None) + expected = components.MeshFaceCoords(face_x=face_x, face_y=None) self.mesh.add_coords(face_x=face_x) self.assertEqual(expected, self.mesh.face_coords) # REPLACE coord. face_x = self.new_coord(self.FACE_LON) - expected = mesh.MeshFaceCoords(face_x=face_x, face_y=None) + expected = components.MeshFaceCoords(face_x=face_x, face_y=None) self.mesh.add_coords(face_x=face_x) self.assertEqual(expected, self.mesh.face_coords) @@ -1132,12 +1143,12 @@ def test_add_coords_single_face(self): def test_dimension_names(self): # Test defaults. - default = mesh.Mesh2DNames("Mesh2d_node", "Mesh2d_edge", "Mesh2d_face") + default = components.Mesh2DNames("Mesh2d_node", "Mesh2d_edge", "Mesh2d_face") self.assertEqual(default, self.mesh.dimension_names()) self.mesh.dimension_names("foo", "bar", "baz") self.assertEqual( - mesh.Mesh2DNames("foo", "bar", "baz"), + components.Mesh2DNames("foo", "bar", "baz"), self.mesh.dimension_names(), ) @@ -1179,7 +1190,7 @@ def test_to_MeshCoord_face(self): location = "face" axis = "x" result = self.mesh.to_MeshCoord(location, axis) - self.assertIsInstance(result, mesh.MeshCoord) + self.assertIsInstance(result, components.MeshCoord) self.assertEqual(location, result.location) self.assertEqual(axis, result.axis) @@ -1190,7 +1201,7 @@ def test_to_MeshCoords_face(self): self.assertEqual(len(self.mesh.AXES), len(result)) for ix, axis in enumerate(self.mesh.AXES): coord = result[ix] - self.assertIsInstance(coord, mesh.MeshCoord) + self.assertIsInstance(coord, components.MeshCoord) self.assertEqual(location, coord.location) self.assertEqual(axis, coord.axis) @@ -1208,7 +1219,7 @@ def test_invalid_topology(self): self.assertRaisesRegex( ValueError, "Expected 'topology_dimension'.*", - mesh.MeshXY, + components.MeshXY, **kwargs, ) @@ -1220,7 +1231,7 @@ def test_invalid_axes(self): self.assertRaisesRegex( ValueError, "Invalid axis specified for node.*", - mesh.MeshXY, + components.MeshXY, node_coords_and_axes=( (self.NODE_LON, "foo"), (self.NODE_LAT, "y"), @@ -1234,14 +1245,14 @@ def test_invalid_axes(self): self.assertRaisesRegex( ValueError, "Invalid axis specified for edge.*", - mesh.MeshXY, + components.MeshXY, edge_coords_and_axes=((self.EDGE_LON, "foo"),), **kwargs, ) self.assertRaisesRegex( ValueError, "Invalid axis specified for face.*", - mesh.MeshXY, + components.MeshXY, face_coords_and_axes=((self.FACE_LON, "foo"),), **kwargs, ) @@ -1261,7 +1272,7 @@ def test_minimum_connectivities(self): self.assertRaisesRegex( ValueError, ".*requires a edge_node_connectivity.*", - mesh.MeshXY, + components.MeshXY, **kwargs, ) @@ -1275,7 +1286,7 @@ def test_minimum_coords(self): self.assertRaisesRegex( ValueError, ".*is a required coordinate.*", - mesh.MeshXY, + components.MeshXY, **kwargs, ) diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py b/lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py similarity index 97% rename from lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py rename to lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py index 8ea9c81060..8114fbe92e 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py +++ b/lib/iris/tests/unit/mesh/components/test_MeshXY__from_coords.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :meth:`iris.experimental.ugrid.mesh.MeshXY.from_coords`.""" +"""Unit tests for the :meth:`iris.mesh.MeshXY.from_coords`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -11,8 +11,7 @@ import numpy as np from iris.coords import AuxCoord, DimCoord -from iris.experimental.ugrid import logger -from iris.experimental.ugrid.mesh import Connectivity, MeshXY +from iris.mesh import Connectivity, MeshXY, logger from iris.tests.stock import simple_2d_w_multidim_coords diff --git a/lib/iris/tests/unit/experimental/ugrid/load/__init__.py b/lib/iris/tests/unit/mesh/utils/__init__.py similarity index 70% rename from lib/iris/tests/unit/experimental/ugrid/load/__init__.py rename to lib/iris/tests/unit/mesh/utils/__init__.py index 3248db6e41..5a252f1529 100644 --- a/lib/iris/tests/unit/experimental/ugrid/load/__init__.py +++ b/lib/iris/tests/unit/mesh/utils/__init__.py @@ -2,4 +2,4 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for the :mod:`iris.experimental.ugrid.load` package.""" +"""Unit tests for the :mod:`iris.mesh.utils` package.""" diff --git a/lib/iris/tests/unit/experimental/ugrid/utils/test_recombine_submeshes.py b/lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py similarity index 99% rename from lib/iris/tests/unit/experimental/ugrid/utils/test_recombine_submeshes.py rename to lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py index 1c0fafdfc9..5323dd5883 100644 --- a/lib/iris/tests/unit/experimental/ugrid/utils/test_recombine_submeshes.py +++ b/lib/iris/tests/unit/mesh/utils/test_recombine_submeshes.py @@ -2,7 +2,7 @@ # # This file is part of Iris and is released under the BSD license. # See LICENSE in the root of the repository for full licensing details. -"""Unit tests for :func:`iris.experimental.ugrid.utils.recombine_submeshes`.""" +"""Unit tests for :func:`iris.mesh.utils.recombine_submeshes`.""" # Import iris.tests first so that some things can be initialised before # importing anything else. @@ -13,7 +13,7 @@ from iris.coords import AuxCoord from iris.cube import CubeList -from iris.experimental.ugrid.utils import recombine_submeshes +from iris.mesh.utils import recombine_submeshes from iris.tests.stock.mesh import sample_mesh, sample_mesh_cube diff --git a/lib/iris/tests/unit/tests/stock/test_netcdf.py b/lib/iris/tests/unit/tests/stock/test_netcdf.py index c218385425..cccb93d28b 100644 --- a/lib/iris/tests/unit/tests/stock/test_netcdf.py +++ b/lib/iris/tests/unit/tests/stock/test_netcdf.py @@ -8,11 +8,11 @@ import tempfile from iris import load_cube -from iris.experimental.ugrid.mesh import MeshCoord, MeshXY # Import iris.tests first so that some things can be initialised before # importing anything else. -import iris.tests as tests +import iris.tests as tests # isort:skip +from iris.mesh import MeshCoord, MeshXY from iris.tests.stock import netcdf