-
Notifications
You must be signed in to change notification settings - Fork 6
Ucubes summaries #27
Ucubes summaries #27
Changes from 6 commits
d2f7b5f
b28fa58
a5b9b6d
fe514e3
e2690de
199c391
eccd444
5e7f4a6
5b0d1bd
dc3665e
8cf8e94
33b8fc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # 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.tests import IrisTest, get_data_path | ||
|
|
||
| from iris.cube import CubeList | ||
|
|
||
| from iris_ugrid.ugrid_cf_reader import load_cubes | ||
|
|
||
|
|
||
| class Test_cube_representations(IrisTest): | ||
| def setUp(self): | ||
| file_path = 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() | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,95 @@ | ||||||
| # 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 and dim == self.ugrid.cube_dim: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The original syntax is nice and efficient, but very easy to misinterpret. Is my suggestion equivalent? And I assume that
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| 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 = [ | ||||||
| ind for ind, char in enumerate(next_line) if char != " " | ||||||
| ][0] | ||||||
|
|
||||||
| # Indent the mesh details 4 spaces more than that. | ||||||
| indent = " " * (indent + 4) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't think the original is written the correct way round. Wouldn't it multiply an existing indent rather than just adding to it?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wrong: I hadn't understood that
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| detail_lines = [indent + 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 | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") | ||
|
|
||
|
|
@@ -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): | ||
| """ | ||
|
|
@@ -179,19 +192,19 @@ 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to say somewhere that this actually returns a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| """ | ||
| # 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)) | ||
|
|
@@ -218,16 +231,34 @@ 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, and replacing it | ||
| # in the caller (and as returned to user). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't follow. To me, it doesn't look like anything is replaced anymore - this function no longer modifies
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have attempted to rephrase in a more explicit way. Believe @pp-mo was referring to the downstream change he introduced to |
||
| # 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, | ||
| ) | ||
|
|
||
|
trexfeathers marked this conversation as resolved.
|
||
| return new_result_cube | ||
|
|
||
|
|
||
| def load_cubes(filenames, callback=None): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this order of these imports.
Why isn't it:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just going to remove the
from iris.tests importaltogether, since we're already importingiris.testson L9.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8cf8e94