Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/iris/experimental/representation.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __init__(self, cube):
self.str_headings = {
"Dimension coordinates:": None,
"Auxiliary coordinates:": None,
"Mesh coordinates:": None,
"Derived coordinates:": None,
"Cell measures:": None,
"Ancillary variables:": None,
Expand All @@ -99,6 +100,7 @@ def __init__(self, cube):
self.dim_desc_coords = [
"Dimension coordinates:",
"Auxiliary coordinates:",
"Mesh coordinates:",
"Derived coordinates:",
"Cell measures:",
"Ancillary variables:",
Expand Down
150 changes: 77 additions & 73 deletions lib/iris/fileformats/netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1418,80 +1418,84 @@ def _add_mesh(self, cube_or_mesh):
cf_mesh_name = self._create_mesh(mesh)
self._name_coord_map.append(cf_mesh_name, mesh)

cf_mesh_var = self._dataset.variables[cf_mesh_name]

# Get the mesh-element dim names.
mesh_dims = self._mesh_dims[mesh]
cf_mesh_var = self._dataset.variables[cf_mesh_name]

# Get the mesh-element dim names.
mesh_dims = self._mesh_dims[mesh]

# Add all the element coordinate variables.
for location in MESH_LOCATIONS:
coords_meshobj_attr = f"{location}_coords"
coords_file_attr = f"{location}_coordinates"
mesh_coords = getattr(mesh, coords_meshobj_attr, None)
if mesh_coords:
coord_names = []
for coord in mesh_coords:
if coord is None:
continue # an awkward thing that mesh.coords does
coord_name = self._create_generic_cf_array_var(
cube_or_mesh,
[],
coord,
element_dims=(mesh_dims[location],),
)
coord_names.append(coord_name)
# Record the coordinates (if any) on the mesh variable.
if coord_names:
coord_names = " ".join(coord_names)
_setncattr(
cf_mesh_var, coords_file_attr, coord_names
)

# Add all the element coordinate variables.
for location in MESH_LOCATIONS:
coords_meshobj_attr = f"{location}_coords"
coords_file_attr = f"{location}_coordinates"
mesh_coords = getattr(mesh, coords_meshobj_attr, None)
if mesh_coords:
coord_names = []
for coord in mesh_coords:
if coord is None:
continue # an awkward thing that mesh.coords does
coord_name = self._create_generic_cf_array_var(
cube_or_mesh,
[],
coord,
element_dims=(mesh_dims[location],),
)
coord_names.append(coord_name)
# Record the coordinates (if any) on the mesh variable.
if coord_names:
coord_names = " ".join(coord_names)
_setncattr(cf_mesh_var, coords_file_attr, coord_names)

# Add all the connectivity variables.
# pre-fetch the set + ignore "None"s, which are empty slots.
conns = [
conn for conn in mesh.all_connectivities if conn is not None
]
for conn in conns:
# Get the connectivity role, = "{loc1}_{loc2}_connectivity".
cf_conn_attr_name = conn.cf_role
loc_from, loc_to, _ = cf_conn_attr_name.split("_")
# Construct a trailing dimension name.
last_dim = f"{cf_mesh_name}_{loc_from}_N_{loc_to}s"
# Create if it does not already exist.
if last_dim not in self._dataset.dimensions:
length = conn.shape[1 - conn.src_dim]
self._dataset.createDimension(last_dim, length)

# Create variable.
# NOTE: for connectivities *with missing points*, this will use a
# fixed standard fill-value of -1. In that case, we create the
# variable with a '_FillValue' property, which can only be done
# when it is first created.
loc_dim_name = mesh_dims[loc_from]
conn_dims = (loc_dim_name, last_dim)
if conn.src_dim == 1:
# Has the 'other' dimension order, =reversed
conn_dims = conn_dims[::-1]
cf_conn_name = self._create_generic_cf_array_var(
cube_or_mesh,
[],
conn,
element_dims=conn_dims,
fill_value=-1,
)
# Add essential attributes to the Connectivity variable.
cf_conn_var = self._dataset.variables[cf_conn_name]
_setncattr(cf_conn_var, "cf_role", cf_conn_attr_name)
_setncattr(cf_conn_var, "start_index", conn.start_index)

# Record the connectivity on the parent mesh var.
_setncattr(cf_mesh_var, cf_conn_attr_name, cf_conn_name)
# If the connectivity had the 'alternate' dimension order, add the
# relevant dimension property
if conn.src_dim == 1:
loc_dim_attr = f"{loc_from}_dimension"
# Should only get here once.
assert loc_dim_attr not in cf_mesh_var.ncattrs()
_setncattr(cf_mesh_var, loc_dim_attr, loc_dim_name)
# Add all the connectivity variables.
# pre-fetch the set + ignore "None"s, which are empty slots.
conns = [
conn
for conn in mesh.all_connectivities
if conn is not None
]
for conn in conns:
# Get the connectivity role, = "{loc1}_{loc2}_connectivity".
cf_conn_attr_name = conn.cf_role
loc_from, loc_to, _ = cf_conn_attr_name.split("_")
# Construct a trailing dimension name.
last_dim = f"{cf_mesh_name}_{loc_from}_N_{loc_to}s"
# Create if it does not already exist.
if last_dim not in self._dataset.dimensions:
length = conn.shape[1 - conn.src_dim]
self._dataset.createDimension(last_dim, length)

# Create variable.
# NOTE: for connectivities *with missing points*, this will use a
# fixed standard fill-value of -1. In that case, we create the
# variable with a '_FillValue' property, which can only be done
# when it is first created.
loc_dim_name = mesh_dims[loc_from]
conn_dims = (loc_dim_name, last_dim)
if conn.src_dim == 1:
# Has the 'other' dimension order, =reversed
conn_dims = conn_dims[::-1]
cf_conn_name = self._create_generic_cf_array_var(
cube_or_mesh,
[],
conn,
element_dims=conn_dims,
fill_value=-1,
)
# Add essential attributes to the Connectivity variable.
cf_conn_var = self._dataset.variables[cf_conn_name]
_setncattr(cf_conn_var, "cf_role", cf_conn_attr_name)
_setncattr(cf_conn_var, "start_index", conn.start_index)

# Record the connectivity on the parent mesh var.
_setncattr(cf_mesh_var, cf_conn_attr_name, cf_conn_name)
# If the connectivity had the 'alternate' dimension order, add the
# relevant dimension property
if conn.src_dim == 1:
loc_dim_attr = f"{loc_from}_dimension"
# Should only get here once.
assert loc_dim_attr not in cf_mesh_var.ncattrs()
_setncattr(cf_mesh_var, loc_dim_attr, loc_dim_name)

return cf_mesh_name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@

from html import escape

import numpy as np

from iris.coords import AncillaryVariable, CellMeasure, CellMethod
from iris.cube import Cube
from iris.experimental.representation import CubeRepresentation
import iris.tests.stock as stock
from iris.tests.stock.mesh import sample_mesh


@tests.skip_data
Expand Down Expand Up @@ -125,7 +129,12 @@ def setUp(self):
self.representer._get_bits(self.representer._get_lines())

def test_population(self):
for v in self.representer.str_headings.values():
nonmesh_values = [
value
for key, value in self.representer.str_headings.items()
if "Mesh" not in key
]
for v in nonmesh_values:
self.assertIsNotNone(v)

def test_headings__dimcoords(self):
Expand Down Expand Up @@ -343,6 +352,17 @@ def setUp(self):
self.representer._get_bits(self.representer._get_lines())
self.result = self.representer._make_content()

# Also provide an ultra-simple mesh cube, with only meshcoords.
mesh = sample_mesh()
meshco_x, meshco_y = mesh.to_MeshCoords("face")
mesh_cube = Cube(np.zeros(meshco_x.shape))
mesh_cube.add_aux_coord(meshco_x, (0,))
mesh_cube.add_aux_coord(meshco_y, (0,))
self.mesh_cube = mesh_cube
self.mesh_representer = CubeRepresentation(self.mesh_cube)
self.mesh_representer._get_bits(self.mesh_representer._get_lines())
self.mesh_result = self.mesh_representer._make_content()

def test_included(self):
included = "Dimension coordinates"
self.assertIn(included, self.result)
Expand All @@ -357,6 +377,23 @@ def test_not_included(self):
for heading in not_included:
self.assertNotIn(heading, self.result)

def test_mesh_included(self):
# self.mesh_cube contains a `Mesh coordinates` section.
included = "Mesh coordinates"
self.assertIn(included, self.mesh_result)
mesh_coord_names = [
c.name() for c in self.mesh_cube.coords(mesh_coords=True)
]
for coord_name in mesh_coord_names:
self.assertIn(coord_name, self.result)

def test_mesh_not_included(self):
# self.mesh_cube _only_ contains a `Mesh coordinates` section.
not_included = list(self.representer.str_headings.keys())
not_included.pop(not_included.index("Mesh coordinates:"))
for heading in not_included:
self.assertNotIn(heading, self.result)


@tests.skip_data
class Test_repr_html(tests.IrisTest):
Expand Down
24 changes: 24 additions & 0 deletions lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,30 @@ def test_multiple_identical_meshes(self):
# Check there are two independent meshes
self._check_two_different_meshes(vars)

def test_multiple_same_mesh(self):
mesh = make_mesh()

# Save and snapshot the result
tempfile_path = self.check_save_mesh([mesh, mesh])
dims, vars = scan_dataset(tempfile_path)

# In this case there should be only *one* mesh.
mesh_names = vars_meshnames(vars)
self.assertEqual(1, len(mesh_names))

# Check it has the correct number of coords + conns (no duplicates)
# Should have 2 each X and Y coords (face+node): _no_ edge coords.
coord_vars_x = vars_w_props(vars, standard_name="longitude")
coord_vars_y = vars_w_props(vars, standard_name="latitude")
self.assertEqual(2, len(coord_vars_x))
self.assertEqual(2, len(coord_vars_y))

# Check the connectivities are all present: _only_ 1 var of each type.
for conn in mesh.all_connectivities:
if conn is not None:
conn_vars = vars_w_props(vars, cf_role=conn.cf_role)
self.assertEqual(1, len(conn_vars))

def test_multiple_different_meshes(self):
# Create 2 meshes with different faces, but same edges.
# N.B. they should *not* then share an edge dimension !
Expand Down