The Mesh Data Model#
This page is intended to summarise the essentials that Iris users need to know about meshes. For exhaustive details on UGRID itself: visit the official UGRID conventions site.
Evolution, not revolution#
Mesh support has been designed wherever possible to fit within the existing Iris model. Meshes concern only the spatial geography of data, and can optionally be limited to just the horizontal geography (e.g. X and Y). Other dimensions such as time or ensemble member (and often vertical levels) retain their familiar structured format.
The UGRID conventions themselves are designed as an addition to the existing CF conventions, which are at the core of Iris’ philosophy.
The mesh format represents data’s geography using an unstructured mesh. This has significant pros and cons when compared to a structured grid.
Assigning data to locations using a structured grid is essentially an act of matching coordinate arrays to each dimension of the data array. The data can also be represented as an area (instead of a point) by including a bounds array for each coordinate array. Figure 1 visualises an example.
A mesh is made up of different types of element:
The ‘core’ of the mesh. A point position in space, constructed from 2 or 3 coordinates (2D or 3D space).
Constructed by connecting 2 nodes.
Constructed by connecting 3 or more nodes.
Constructed by connecting 4 or more nodes (which must each have 3 coordinates - 3D space).
Every node in the mesh is defined by indexing the 1-dimensional X and Y (and
optionally Z) coordinate arrays (the
node_coordinates) - e.g.
(x, y) gives the position of the fourth node. Note that this means
each node has its own coordinates, independent of every other node.
Any higher dimensional element - an edge/face/volume - is described by a
sequence of the indices of the nodes that make up that element. E.g. a
triangular face made from connecting the first, third and fourth nodes:
[0, 2, 3]. These 1D sequences combine into a 2D array enumerating all
the elements of that type - edge/face/volume - called a connectivity.
E.g. we could make a mesh of 4 nodes, with 2 triangles described using this
[[0, 2, 3], [3, 2, 1]] (note the shared nodes).
More on Connectivities:
The element type described by a connectivity is known as its location;
According to the UGRID conventions, the nodes in a face should be listed in “anti-clockwise order from above”.
Connectivities also exist to connect the higher dimensional elements, e.g.
face_edge_connectivity. These are optional conveniences to speed up certain operations and will not be discussed here.
Meshes are unstructured. The mesh elements - represented in the coordinate and connectivity arrays detailed above - are enumerated along a single unstructured dimension. An element’s position along this dimension has nothing to do with its spatial position.
A data variable associated with a mesh has a location of either
volume. The data is stored in a 1D array with one
datum per element, matched to its element by matching the datum index with the
coordinate or connectivity index along the unstructured dimension. So for
an example data array called
foo would be at position
(x, y) if it were node-located, or at
faces if it were face-located. Figure 2 visualises an
example of what is described above.
The mesh model also supports edges/faces/volumes having associated ‘centre’ coordinates - to allow point data to be assigned to these elements. ‘Centre’ is just a convenience term - the points can exist anywhere within their respective elements. See Figure 3 for a visualised example.
Above we have seen how one could replicate data on a structured grid using a mesh instead. But the utility of a mesh is the extra flexibility it offers. Here are the main examples:
Every node is completely independent - every one can have unique X andY (and Z) coordinate values. See Figure 4.
Faces and volumes can have variable node counts, i.e. different numbers of sides. This is achieved by masking the unused ‘slots’ in the connectivity array. See Figure 5.
Data can be assigned to lines (edges) just as easily as points (nodes) or areas (faces). See Figure 6.
The highly specific way of recording position (geometry) and shape (topology) allows meshes to represent essentially any spatial arrangement of data. There are therefore many new applications that aren’t possible using a structured grid, including:
Coordinates are recorded per-node, and connectivities are recorded per-element. This is opposed to a structured grid, where a single coordinate value is shared by every data point/area along that line.
For example: representing the surface of a cubed-sphere using a mesh leads to coordinates and connectivities being ~8 times larger than the data itself, as opposed to a small fraction of the data size when dividing a spherical surface using a structured grid of longitudes and latitudes.
This further increases the emphasis on lazy loading and processing of data using packages such as Dask.
The large, 1D data arrays associated with meshes are a very different
shape to what Iris users and developers are used to. It is suspected
that optimal performance will need new chunking strategies, but at time
of writing (
Jan 2022) experience is still limited.
Detail: Working with Mesh Data
Indexing a mesh data array cannot be used for:
This is because - unlike with a structured data array - relative position in a mesh’s 1-dimensional data arrays has no relation to relative position in space. We must instead perform specialised operations using the information in the mesh’s connectivities, or by translating the mesh into a format designed for mesh analysis such as VTK.
Such calculations can still be optimised to avoid them slowing workflows, but the important take-away here is that adaptation is needed when working mesh data.
How Iris Represents This#
Remember this is a prose summary. Precise documentation is at:
At time of writing (
Jan 2022), neither 3D meshes nor 3D elements
(volumes) are supported.
Cube has several new members:
Cube's unstructured dimension has multiple attached
iris.experimental.ugrid.MeshCoords (one for each axis e.g.
y), which can be used to infer the points and bounds of any index on
Cube's unstructured dimension.
>>> print(edge_cube) edge_data / (K) (-- : 6; height: 3) Dimension coordinates: height - x Mesh coordinates: latitude x - longitude x - Mesh: name my_mesh location edge >>> print(edge_cube.location) edge >>> print(edge_cube.mesh_dim()) 0 >>> print(edge_cube.mesh.summary(shorten=True)) <Mesh: 'my_mesh'>
How UGRID information is stored#
- Contains all information about the mesh.Includes:
1-3 collections of
node_coordsThe nodes that are the basis for the mesh.
1 or more
- Required for 1D (edge) elements:
edge_node_connectivityDefine the edges by connecting nodes.
- Required for 2D (face) elements:
face_node_connectivityDefine the faces by connecting nodes.
Optional: any other connectivity type. See
iris.experimental.ugrid.mesh.Connectivity.UGRID_CF_ROLESfor the full list of types.
>>> print(edge_cube.mesh) Mesh : 'my_mesh' topology_dimension: 2 node node_dimension: 'Mesh2d_node' node coordinates <AuxCoord: longitude / (degrees_east) [...] shape(5,)> <AuxCoord: latitude / (degrees_north) [...] shape(5,)> edge edge_dimension: 'Mesh2d_edge' edge_node_connectivity: <Connectivity: unknown / (unknown) [...] shape(6, 2)> edge coordinates <AuxCoord: longitude / (degrees_east) [...] shape(6,)> <AuxCoord: latitude / (degrees_north) [...] shape(6,)> face face_dimension: 'Mesh2d_face' face_node_connectivity: <Connectivity: unknown / (unknown) [...] shape(2, 4)> face coordinates <AuxCoord: longitude / (degrees_east) [...] shape(2,)> <AuxCoord: latitude / (degrees_north) [...] shape(2,)> long_name: 'my_mesh'
- Described in detail in MeshCoords.Stores the following information:
Cube to a
attaching to the
Cube's unstructured dimension, in the
same way that all
Coords attach to
Cube dimensions. This allows a single
Cube to have a combination of unstructured and structured
dimensions (e.g. horizontal mesh plus vertical levels and a time series),
using the same logic for every dimension.
MeshCoords are instantiated using a given
axis. The process interprets the
node_coords and if appropriate the
to produce a
representation of all the
nodes/edges/faces for the given axis.
>>> for coord in edge_cube.coords(mesh_coords=True): ... print(coord) MeshCoord : latitude / (degrees_north) mesh: <Mesh: 'my_mesh'> location: 'edge' points: [3. , 1.5, 1.5, 1.5, 0. , 0. ] bounds: [ [3., 3.], [3., 0.], [3., 0.], [3., 0.], [0., 0.], [0., 0.]] shape: (6,) bounds(6, 2) dtype: float64 standard_name: 'latitude' axis: 'y' MeshCoord : longitude / (degrees_east) mesh: <Mesh: 'my_mesh'> location: 'edge' points: [2.5, 0. , 5. , 6.5, 2.5, 6.5] bounds: [ [0., 5.], [0., 0.], [5., 5.], [5., 8.], [0., 5.], [5., 8.]] shape: (6,) bounds(6, 2) dtype: float64 standard_name: 'longitude' axis: 'x'