Skip to content
This repository was archived by the owner on Jul 8, 2021. It is now read-only.
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
16 changes: 14 additions & 2 deletions iris_ugrid/tests/integration/test_ugrid_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
from gridded.pyugrid.ugrid import UGrid

from iris import Constraint
from iris.cube import CubeList
from iris.cube import CubeList, Cube

from iris_ugrid.ucube import UCube
from iris_ugrid.ugrid_cf_reader import CubeUgrid, load_cubes


Expand All @@ -38,9 +40,9 @@ def test_basic_load(self):
self.assertEqual(len(loaded_cubes), 2)

(cube_0,) = loaded_cubes.extract(Constraint("theta"))
(cube_1,) = loaded_cubes.extract(Constraint("radius"))

# Check the primary cube.
self.assertIsInstance(cube_0, UCube)
self.assertEqual(cube_0.var_name, "theta")
self.assertEqual(cube_0.long_name, "Potential Temperature")
self.assertEqual(cube_0.shape, (1, 6, 866))
Expand All @@ -63,6 +65,16 @@ def test_basic_load(self):
self.assertIsInstance(ugrid, UGrid)
self.assertEqual(ugrid.mesh_name, "Mesh0")

def test_nonugrid_load(self):
# Check that ugrid-load can still return "ordinary" cubes.
file_path = tests.get_data_path(
("NetCDF", "rotated", "xy", "rotPole_landAreaFraction.nc")
)
cubes = CubeList(load_cubes(file_path))
cube = cubes[0]
self.assertIsInstance(cube, Cube)
self.assertFalse(isinstance(cube, UCube))


if __name__ == "__main__":
tests.main()
86 changes: 86 additions & 0 deletions iris_ugrid/tests/unit/ucube/test_UCube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright Iris-ugrid contributors
#
# This file is part of Iris and is released under the LGPL license.
# See COPYING and COPYING.LESSER in the root of the repository for full
# licensing details.
"""
Test basic :class:`iris_ugrid.ucube.Ucube` object.
"""
import iris.tests as tests

import re

from iris import Constraint
from iris.cube import CubeList

from iris_ugrid.ugrid_cf_reader import load_cubes


class Test_cube_representations(tests.IrisTest):
def setUp(self):
file_path = tests.get_data_path(
("NetCDF", "unstructured_grid", "theta_nodal_xios.nc")
)
loaded_cubes = CubeList(load_cubes(file_path))
(cube,) = loaded_cubes.extract(Constraint("theta"))
# Prune the attributes, just because there are a lot.
keep_attrs = ["timeStamp", "Conventions"]
cube.attributes = {
key: value
for key, value in cube.attributes.items()
if key in keep_attrs
}
self.ucube = cube

def test_summary_short(self):
# Check the short-form of a UCube summary.
# This the same as what will appear in a CubeList string repr.
result = self.ucube.summary(shorten=True)
expected = (
"Potential Temperature / (K) "
"(time: 1; levels: 6; *-- : 866)"
)
self.assertEqual(result, expected)

def test_summary_long(self):
result = str(self.ucube)
expected = """\
Potential Temperature / (K) (time: 1; levels: 6; *-- : 866)
Dimension coordinates:
time x - -
levels - x -
Auxiliary coordinates:
time x - -
Unstructured mesh:
Mesh0.node - - x
topology_dimension "2" :
node_coordinates "latitude longitude" :
<unprintable mesh>
Attributes:
Conventions: UGRID
timeStamp: 2016-Oct-24 15:16:48 BST
Cell methods:
point: time\
"""
self.assertEqual(result, expected)

def test__repr_html_(self):
result = self.ucube._repr_html_()
# Check for some key pieces of html, which indicate that it includes
# a summary of the unstructured dimension, and mesh details.
str_dim = '<th class="iris iris-word-cell">*--</th>'
self.assertIn(str_dim, result)
str_section = (
'<td class="iris-title iris-word-cell">Unstructured mesh</td>'
)
self.assertIn(str_section, result)
re_mesh = (
r'<td class="iris-word-cell iris-subheading-cell">'
r"\s*Mesh0\s*</td>"
r'\s*<td class="iris-inclusion-cell">node</td>'
)
self.assertIsNotNone(re.search(re_mesh, result))


if __name__ == "__main__":
tests.main()
97 changes: 97 additions & 0 deletions iris_ugrid/ucube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright Iris contributors
#
# This file is part of Iris and is released under the LGPL license.
# See COPYING and COPYING.LESSER in the root of the repository for full
# licensing details.
"""
Defines the UCube : a cube which has an unstructured mesh dimension.

"""
from iris.cube import Cube


class UCube(Cube):
# Derived 'unstructured' Cube subtype, with a '.ugrid' property.
def __init__(self, *args, ugrid=None, **kwargs):
super().__init__(*args, **kwargs)
self.ugrid = ugrid

def _summary_dim_name(self, dim):
"""
Add an identifying "*" prefix to the mesh dimension.

This specialises the labelling of dims in cube summaries.

"""
name = super()._summary_dim_name(dim)
if self.ugrid is not None and dim == self.ugrid.cube_dim:
name = "*" + name
return name

def _summary_vector_sections_info(self):
"""
Build the "vector summary sections" list. This has the standard form,
plus one extra section to contain the mesh.

This extends cube summaries with a row showing the mesh as for a
coordinate, showing which cube dims it maps to.

"""
specs = super()._summary_vector_sections_info()
if self.ugrid:
Spec = Cube._VectorSectionSpec
specs.append(
Spec(
title="Unstructured mesh",
elements=[self.ugrid],
add_extra_lines=True,
)
)
return specs

def summary(self, shorten=False, *args, **kwargs):
"""
Provide cube summaries, extended to include mesh information.

"""
summary = super().summary(shorten=shorten, *args, **kwargs)
if self.ugrid and not shorten:
# Get a mesh description : as it prints itself.
detail_lines = str(self.ugrid).split("\n")
# Use only certain parts: which happens to be the last N lines.
(i_wanted_line,) = [
i
for i, line in enumerate(detail_lines)
if "topology_dimension" in line
]
# Cut out end portion, strip lines and discard blank ones.
detail_lines = detail_lines[i_wanted_line:]
detail_lines = [line.strip() for line in detail_lines]
detail_lines = [line for line in detail_lines if line]

# Find the section that shows the grid info.
summary_lines = summary.split("\n")
ugrid_section_title = "Unstructured mesh"
(i_ugrid_line,) = [
i
for i, line in enumerate(summary_lines)
if line.strip().startswith(ugrid_section_title)
]

# Get the indent of the line below (the grid variable dims).
next_line = summary_lines[i_ugrid_line + 1]
indent_number = [
ind for ind, char in enumerate(next_line) if char != " "
][0]

# Indent the mesh details 4 spaces more than that.
indent_string = " " * (indent_number + 4)
detail_lines = [indent_string + line for line in detail_lines]

# Splice in the detail lines after that, indenting to match.
i_next_section = i_ugrid_line + 2
summary_lines[i_next_section:i_next_section] = detail_lines

summary = "\n".join(summary_lines)

return summary
58 changes: 49 additions & 9 deletions iris_ugrid/ugrid_cf_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from iris.fileformats.cf import CFReader
import iris.fileformats.netcdf

from iris_ugrid.ucube import UCube

_UGRID_ELEMENT_TYPE_NAMES = ("node", "edge", "face", "volume")

Expand Down Expand Up @@ -100,6 +101,18 @@ def __str__(self):
def name(self):
return ".".join([self.grid.mesh_name, self.mesh_location])

def cube_dims(self, cube):
# This is needed for cube summary generation, because this object is
# included as a "cube element" in the list structure returned by
# :meth:`UCube._summary_vector_sections_info`.
# All the other elements are _DimensionalMetadata objects.
# Hopefully this will be the only aspect of those which we must mimic.
if self.cube_dim is None:
result = ()
else:
result = (self.cube_dim,)
return result


class UGridCFReader(CFReader):
"""
Expand Down Expand Up @@ -179,19 +192,27 @@ def __init__(self, filename, *args, **kwargs):

def cube_completion_adjust(self, cube):
"""
Cube post-processing method to add details of the mesh to any newly
created cubes which have a mesh dimension.
Cube post-processing method convert newly created cubes which have a
a mesh dimension into :class:`UCubes`s.

Called by a 'cube post-modify hook' in
:func:`iris.fileformats.netcdf.load_cubes`.

Adds the ".ugrid" property to cubes created by the CF reader, which
links the cube mesh dimension to a specific mesh and element-type (aka
"mesh_location").
Constructs a :class:`CubeUgrid` referencing the appropriate file mesh,
and makes a new `UCube` of which this is the '.ugrid' property.
Comment on lines +201 to +202
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to say somewhere that this actually returns a UCube.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Args:

* cube (:class:`iris.cube.Cube`):
The cube to be post-processed

Returns:
:class:`iris_ugrid.ucube.UCube` or ``None``

"""
# Identify the unstructured-grid dimension of the cube (if any), and
# attach a suitable CubeUgrid object
new_result_cube = None
data_var = self.dataset.variables[cube.var_name]
meshes_info = [
(i_dim, self.meshdims_map.get(dim_name))
Expand All @@ -218,16 +239,35 @@ def cube_completion_adjust(self, cube):
)
node_coordinates.append(name)

cube.ugrid = CubeUgrid(
cube_ugrid = CubeUgrid(
cube_dim=i_dim,
grid=mesh,
mesh_location=mesh_location,
topology_dimension=topology_dimension,
node_coordinates=sorted(node_coordinates),
)
else:
# Add an empty 'cube.ugrid' to all cubes otherwise.
cube.ugrid = None
# Return a new UCube, based on the provided Cube. Relying on the
# caller (e.g. :func:iris.fileformats.netcdf.load_cubes) to
# appropriately handle any replacement of the original Cube.
# Absolutely **everything** is the same, except for the extra ugrid
# property.
new_result_cube = UCube(
data=cube.core_data(),
standard_name=cube.standard_name,
long_name=cube.long_name,
var_name=cube.var_name,
units=cube.units,
attributes=cube.attributes,
cell_methods=cube.cell_methods,
dim_coords_and_dims=cube._dim_coords_and_dims,
aux_coords_and_dims=cube._aux_coords_and_dims,
aux_factories=cube.aux_factories,
cell_measures_and_dims=cube._cell_measures_and_dims,
ancillary_variables_and_dims=cube._ancillary_variables_and_dims,
ugrid=cube_ugrid,
)

Comment thread
trexfeathers marked this conversation as resolved.
return new_result_cube


def load_cubes(filenames, callback=None):
Expand Down