Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
34 changes: 33 additions & 1 deletion lib/iris/experimental/ugrid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3656,7 +3656,39 @@ def _build_mesh(cf, mesh_var, file_path):
attributes = {}
attr_units = get_attr_units(mesh_var, attributes)

topology_dimension = mesh_var.topology_dimension
if hasattr(mesh_var, "volume_node_connectivity"):
topology_dimension = 3
elif hasattr(mesh_var, "face_node_connectivity"):
topology_dimension = 2
elif hasattr(mesh_var, "edge_node_connectivity"):
topology_dimension = 1
else:
# Nodes only. We aren't sure yet whether this is a valid option.
topology_dimension = 0

if not hasattr(mesh_var, "topology_dimension"):
msg = (
f"Mesh variable {mesh_var.cf_name} has no 'topology_dimension'"
f" : *Assuming* topology_dimension={topology_dimension}"
", consistent with the attached connectivities."
)
# TODO: reconsider logging level when we have consistent practice.
# TODO: logger always requires extras['cls'] : can we fix this?
logger.warning(msg, extra=dict(cls=None))
else:
quoted_topology_dimension = mesh_var.topology_dimension
if quoted_topology_dimension != topology_dimension:
msg = (
f"*Assuming* 'topology_dimension'={topology_dimension}"
f", from the attached connectivities of the mesh variable "
f"{mesh_var.cf_name}. However, "
f"{mesh_var.cf_name}:topology_dimension = "
f"{quoted_topology_dimension}"
" -- ignoring this as it is inconsistent."
)
# TODO: reconsider logging level when we have consistent practice.
# TODO: logger always requires extras['cls'] : can we fix this?
logger.warning(msg=msg, extra=dict(cls=None))

node_dimension = None
edge_dimension = getattr(mesh_var, "edge_dimension", None)
Expand Down
70 changes: 69 additions & 1 deletion lib/iris/tests/integration/experimental/test_ugrid_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
from collections.abc import Iterable

from iris import Constraint, load
from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD
from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD, logger

from iris.tests.stock.netcdf import (
_file_from_cdl_template as create_file_from_cdl_template,
)
from iris.tests.unit.tests.stock.test_netcdf import XIOSFileMixin


def ugrid_load(uris, constraints=None, callback=None):
Expand Down Expand Up @@ -117,3 +122,66 @@ def test_multiple_phenomena(self):
self.assertCML(
cube_list, ("experimental", "ugrid", "surface_mean.cml")
)


class TestTolerantLoading(XIOSFileMixin):
# N.B. using parts of the XIOS-like file integration testing, to make
# temporary netcdf files from stored CDL templates.
@classmethod
def setUpClass(cls):
super().setUpClass() # create cls.temp_dir = dir for test files

@classmethod
def tearDownClass(cls):
super().setUpClass() # destroy temp dir

# Create a testfile according to testcase-specific arguments.
# NOTE: with this, parent "create_synthetic_test_cube" can load a cube.
def create_synthetic_file(self, **create_kwargs):
template_name = create_kwargs["template"] # required kwarg
testfile_name = "tmp_netcdf"
template_subs = dict(
NUM_NODES=7, NUM_FACES=3, DATASET_NAME=testfile_name
)
kwarg_subs = create_kwargs.get("subs", {}) # optional kwarg
template_subs.update(kwarg_subs)
filepath = create_file_from_cdl_template(
temp_file_dir=self.temp_dir,
dataset_name=testfile_name,
dataset_type=template_name,
template_subs=template_subs,
)
return str(filepath) # N.B. Path object not usable in iris.load

def test_mesh_bad_topology_dimension(self):
# Check that the load generates a suitable warning.
template = "minimal_bad_topology_dim"
dim_line = "mesh_var:topology_dimension = 1 ;" # which is wrong !
with self.assertLogs(logger, level="DEBUG") as log:
cube = self.create_synthetic_test_cube(
template=template, subs=dict(TOPOLOGY_DIM_DEFINITION=dim_line)
)
self.assertEqual(len(log.output), 1)
re_msg = r"topology_dimension.* ignoring"
self.assertRegexpMatches(log.output[0], re_msg)

# Check that the result has topology-dimension of 2 (not 1).
self.assertEqual(cube.mesh.topology_dimension, 2)

def test_mesh_no_topology_dimension(self):
# Check that the load generates a suitable warning.
template = "minimal_bad_topology_dim"
dim_line = "" # don't create ANY topology_dimension property
with self.assertLogs(logger, level="DEBUG") as log:
cube = self.create_synthetic_test_cube(
template=template, subs=dict(TOPOLOGY_DIM_DEFINITION=dim_line)
)
self.assertEqual(len(log.output), 1)
re_msg = r"Mesh variable.* has no 'topology_dimension'"
self.assertRegexpMatches(log.output[0], re_msg)
# Check that the result has the correct topology-dimension value.
self.assertEqual(cube.mesh.topology_dimension, 2)


if __name__ == "__main__":
tests.main()
38 changes: 38 additions & 0 deletions lib/iris/tests/stock/file_headers/minimal_bad_topology_dim.cdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Tolerant loading test example : the mesh has the wrong 'topology_dimension'
// NOTE: *not* truly minimal, as we cannot (yet) handle data with no face coords.
netcdf ${DATASET_NAME} {
dimensions:
NODES = ${NUM_NODES} ;
FACES = ${NUM_FACES} ;
FACE_CORNERS = 4 ;
variables:
int mesh_var ;
mesh_var:cf_role = "mesh_topology" ;
${TOPOLOGY_DIM_DEFINITION}
mesh_var:node_coordinates = "mesh_node_x mesh_node_y" ;
mesh_var:face_node_connectivity = "mesh_face_nodes" ;
mesh_var:face_coordinates = "mesh_face_x mesh_face_y" ;
float mesh_node_x(NODES) ;
mesh_node_x:standard_name = "longitude" ;
mesh_node_x:long_name = "Longitude of mesh nodes." ;
mesh_node_x:units = "degrees_east" ;
float mesh_node_y(NODES) ;
mesh_node_y:standard_name = "latitude" ;
mesh_node_y:long_name = "Latitude of mesh nodes." ;
mesh_node_y:units = "degrees_north" ;
float mesh_face_x(FACES) ;
mesh_face_x:standard_name = "longitude" ;
mesh_face_x:long_name = "Longitude of mesh nodes." ;
mesh_face_x:units = "degrees_east" ;
float mesh_face_y(FACES) ;
mesh_face_y:standard_name = "latitude" ;
mesh_face_y:long_name = "Latitude of mesh nodes." ;
mesh_face_y:units = "degrees_north" ;
int mesh_face_nodes(FACES, FACE_CORNERS) ;
mesh_face_nodes:cf_role = "face_node_connectivity" ;
mesh_face_nodes:long_name = "Maps every face to its corner nodes." ;
mesh_face_nodes:start_index = 0 ;
float data_var(FACES) ;
data_var:mesh = "mesh_var" ;
data_var:location = "face" ;
}