-
Notifications
You must be signed in to change notification settings - Fork 300
Ugrid save #4303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Ugrid save #4303
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
bcc643e
Refactor onto a common multidim var routine.
pp-mo 9ad240e
Ugrid save - first working.
pp-mo 8791ff5
Required mesh attributes, loadback check.
pp-mo d22e1da
Odd fixes to netcdf-save.
pp-mo fdd3937
Added tests based on UGRID cdl examples.
pp-mo 2232350
Remove bad comment.
pp-mo a76bed3
Start on specific tests.
pp-mo 60899a9
First proper result checking.
pp-mo 94bc780
Slight tidy, drop _MINIMAL_MESH, new testing util routines.
pp-mo 3e188bd
Small changes.
pp-mo 90c736a
Identify mesh-location-dims with element-coord/connectivity.
pp-mo c6f8a58
Fix equality between connectivities of different shapes.
pp-mo 8da2da4
Small test fix.
pp-mo 5284416
Tracking of mesh dims, including disambiguated: Working with existing…
pp-mo e77385e
Ugrid example #4: hybrid coord is on vertical dim *not* mesh.
pp-mo e711368
Avoid non-ugrid problem in integration roundtrip 'ex4' test.
pp-mo cb357c8
Fix example files readme.
pp-mo e173ac0
More tests - working.
pp-mo 53f65dd
Interim fix to cube summary.
pp-mo 50f39ba
Interim fix connectivity comparison.
pp-mo 931475b
Support connectivity missing indices and alternate dims - no test for…
pp-mo 8a214d1
Remove debug.
pp-mo b3d1bc6
Move complex additional actions from _create_mesh into _add_mesh.
pp-mo ead5077
Support+test connectivities with missing points.
pp-mo 551367e
Code moved from _create_mesh to _add_mesh needs to be inside conditio…
pp-mo 8df8750
Updated lockfiles.
pp-mo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -522,6 +522,8 @@ def __eq__(self, other): | |
| if hasattr(other, "metadata"): | ||
| # metadata comparison | ||
| eq = self.metadata == other.metadata | ||
| if eq: | ||
| eq = self.shape == other.shape | ||
|
Member
Author
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. This fixes it so that connectivities of different shapes can compare correctly (i.e. returns False). |
||
| if eq: | ||
| eq = ( | ||
| self.indices_by_src() == other.indices_by_src() | ||
|
|
||
Large diffs are not rendered by default.
Oops, something went wrong.
128 changes: 128 additions & 0 deletions
128
lib/iris/tests/integration/experimental/test_ugrid_save.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # 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. | ||
| """ | ||
| Integration tests for NetCDF-UGRID file saving. | ||
|
|
||
| """ | ||
| # Import iris.tests first so that some things can be initialised before | ||
| # importing anything else. | ||
| import iris.tests as tests # isort:skip | ||
|
|
||
| import glob | ||
| from pathlib import Path | ||
| import shutil | ||
| from subprocess import check_call | ||
| import tempfile | ||
|
|
||
| import iris | ||
| from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD | ||
| import iris.fileformats.netcdf | ||
| from iris.tests import IrisTest | ||
| from iris.tests.stock.netcdf import _add_standard_data | ||
|
|
||
|
|
||
| class TestBasicSave(IrisTest): | ||
| @classmethod | ||
| def setUpClass(cls): | ||
| cls.temp_dir = Path(tempfile.mkdtemp()) | ||
| cls.examples_dir = ( | ||
| Path(__file__).absolute().parent / "ugrid_conventions_examples" | ||
| ) | ||
| example_paths = glob.glob(str(cls.examples_dir / "*ex*.cdl")) | ||
| example_names = [ | ||
| str(Path(filepath).name).split("_")[1] # = "ex<N>" | ||
| for filepath in example_paths | ||
| ] | ||
| cls.example_names_paths = { | ||
| name: path for name, path in zip(example_names, example_paths) | ||
| } | ||
|
|
||
| @classmethod | ||
| def tearDownClass(cls): | ||
| shutil.rmtree(cls.temp_dir) | ||
|
|
||
| def test_example_result_cdls(self): | ||
| # Snapshot the result of saving the example cases. | ||
| for ex_name, filepath in self.example_names_paths.items(): | ||
| target_ncfile_path = str(self.temp_dir / f"{ex_name}.nc") | ||
| # Create a netcdf file from the test CDL. | ||
| check_call( | ||
| f"ncgen {filepath} -k4 -o {target_ncfile_path}", shell=True | ||
| ) | ||
| # Fill in blank data-variables. | ||
| _add_standard_data(target_ncfile_path) | ||
| # Load as Iris data | ||
| with PARSE_UGRID_ON_LOAD.context(): | ||
| cubes = iris.load(target_ncfile_path) | ||
| # Re-save, to check the save behaviour. | ||
| resave_ncfile_path = str(self.temp_dir / f"{ex_name}_resaved.nc") | ||
| iris.save(cubes, resave_ncfile_path) | ||
| # Check the output against a CDL snapshot. | ||
| refdir_relpath = ( | ||
| "integration/experimental/ugrid_save/TestBasicSave/" | ||
| ) | ||
| reffile_name = str(Path(filepath).name).replace(".nc", ".cdl") | ||
| reffile_path = refdir_relpath + reffile_name | ||
| self.assertCDL(resave_ncfile_path, reference_filename=reffile_path) | ||
|
|
||
| def test_example_roundtrips(self): | ||
| # Check that save-and-loadback leaves Iris data unchanged, | ||
| # for data derived from each UGRID example CDL. | ||
| for ex_name, filepath in self.example_names_paths.items(): | ||
| print(f"Roundtrip checking : {ex_name}") | ||
| target_ncfile_path = str(self.temp_dir / f"{ex_name}.nc") | ||
| # Create a netcdf file from the test CDL. | ||
| check_call( | ||
| f"ncgen {filepath} -k4 -o {target_ncfile_path}", shell=True | ||
| ) | ||
| # Fill in blank data-variables. | ||
| _add_standard_data(target_ncfile_path) | ||
| # Load the original as Iris data | ||
| with PARSE_UGRID_ON_LOAD.context(): | ||
| orig_cubes = iris.load(target_ncfile_path) | ||
|
|
||
| if "ex4" in ex_name: | ||
| # Discard the extra formula terms component cubes | ||
| # Saving these does not do what you expect | ||
| orig_cubes = orig_cubes.extract("datavar") | ||
|
|
||
| # Save-and-load-back to compare the Iris saved result. | ||
| resave_ncfile_path = str(self.temp_dir / f"{ex_name}_resaved.nc") | ||
| iris.save(orig_cubes, resave_ncfile_path) | ||
| with PARSE_UGRID_ON_LOAD.context(): | ||
| savedloaded_cubes = iris.load(resave_ncfile_path) | ||
|
|
||
| # This should match the original exactly | ||
| # ..EXCEPT for our inability to compare meshes. | ||
| for orig, reloaded in zip(orig_cubes, savedloaded_cubes): | ||
| for cube in (orig, reloaded): | ||
| # Remove conventions attributes, which may differ. | ||
| cube.attributes.pop("Conventions", None) | ||
| # Remove var-names, which may differ. | ||
| cube.var_name = None | ||
|
|
||
| # Compare the mesh contents (as we can't compare actual meshes) | ||
| self.assertEqual(orig.location, reloaded.location) | ||
| orig_mesh = orig.mesh | ||
| reloaded_mesh = reloaded.mesh | ||
| self.assertEqual( | ||
| orig_mesh.all_coords, reloaded_mesh.all_coords | ||
| ) | ||
| self.assertEqual( | ||
| orig_mesh.all_connectivities, | ||
| reloaded_mesh.all_connectivities, | ||
| ) | ||
| # Index the cubes to replace meshes with meshcoord-derived aux coords. | ||
| # This needs [:0] on the mesh dim, so do that on all dims. | ||
| keys = tuple([slice(0, None)] * orig.ndim) | ||
| orig = orig[keys] | ||
| reloaded = reloaded[keys] | ||
| # Resulting cubes, with collapsed mesh, should be IDENTICAL. | ||
| self.assertEqual(orig, reloaded) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| tests.main() |
16 changes: 16 additions & 0 deletions
16
lib/iris/tests/integration/experimental/ugrid_conventions_examples/README.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| Examples generated from CDL example sections in UGRID conventions v1.0 | ||
pp-mo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ( see webpage: https://ugrid-conventions.github.io/ugrid-conventions/ ) | ||
|
|
||
| CHANGES: | ||
| * added a data-var to all examples, for ease of iris-roundtripping | ||
| * EX4 : | ||
| - had a couple of missing ";"s at lineends | ||
| - the formula terms (depth+surface) should map to 'Mesh2_layers', and not to the mesh at all. | ||
| - use Mesh2d_layers dim, and have no 'mesh' or 'location' | ||
| * "EX4a" -- possibly (future) closer mix of hybrid-vertical and mesh dimensions | ||
| - *don't* think we can have a hybrid coord ON the mesh dimension | ||
| - mesh being a vertical location (only) seems to make no sense | ||
| - .. and implies that the mesh is 1d and ordered, which is not really unstructured at all | ||
| - *could* have hybrid-height with the _orography_ mapping to the mesh | ||
| - doesn't match the UGRID examples, but see : iris.tests.unit.fileformats.netcdf.test_Saver__ugrid.TestSaveUgrid__cube.test_nonmesh_hybrid_dim | ||
|
|
||
55 changes: 55 additions & 0 deletions
55
lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| netcdf ex1_1d_mesh { | ||
| dimensions: | ||
| nMesh1_node = 5 ; // nNodes | ||
| nMesh1_edge = 4 ; // nEdges | ||
|
|
||
| Two = 2; | ||
|
|
||
| variables: | ||
| // Mesh topology | ||
| integer Mesh1 ; | ||
| Mesh1:cf_role = "mesh_topology" ; | ||
| Mesh1:long_name = "Topology data of 1D network" ; | ||
| Mesh1:topology_dimension = 1 ; | ||
| Mesh1:node_coordinates = "Mesh1_node_x Mesh1_node_y" ; | ||
| Mesh1:edge_node_connectivity = "Mesh1_edge_nodes" ; | ||
| Mesh1:edge_coordinates = "Mesh1_edge_x Mesh1_edge_y" ; // optional attribute | ||
| integer Mesh1_edge_nodes(nMesh1_edge, Two) ; | ||
| Mesh1_edge_nodes:cf_role = "edge_node_connectivity" ; | ||
| Mesh1_edge_nodes:long_name = "Maps every edge/link to the two nodes that it connects." ; | ||
| Mesh1_edge_nodes:start_index = 1 ; | ||
|
|
||
| // Mesh node coordinates | ||
| double Mesh1_node_x(nMesh1_node) ; | ||
| Mesh1_node_x:standard_name = "longitude" ; | ||
| Mesh1_node_x:long_name = "Longitude of 1D network nodes." ; | ||
| Mesh1_node_x:units = "degrees_east" ; | ||
| double Mesh1_node_y(nMesh1_node) ; | ||
| Mesh1_node_y:standard_name = "latitude" ; | ||
| Mesh1_node_y:long_name = "Latitude of 1D network nodes." ; | ||
| Mesh1_node_y:units = "degrees_north" ; | ||
|
|
||
| // Optional mesh edge coordinate variables | ||
| double Mesh1_edge_x(nMesh1_edge) ; | ||
| Mesh1_edge_x:standard_name = "longitude" ; | ||
| Mesh1_edge_x:long_name = "Characteristic longitude of 1D network edge (e.g. midpoint of the edge)." ; | ||
| Mesh1_edge_x:units = "degrees_east" ; | ||
| Mesh1_edge_x:bounds = "Mesh1_edge_xbnds" ; | ||
| double Mesh1_edge_y(nMesh1_edge) ; | ||
| Mesh1_edge_y:standard_name = "latitude" ; | ||
| Mesh1_edge_y:long_name = "Characteristic latitude of 1D network edge (e.g. midpoint of the edge)." ; | ||
| Mesh1_edge_y:units = "degrees_north" ; | ||
| Mesh1_edge_y:bounds = "Mesh1_edge_ybnds" ; | ||
| double Mesh1_edge_xbnds(nMesh1_edge,Two) ; | ||
| Mesh1_edge_xbnds:standard_name = "longitude" ; | ||
| Mesh1_edge_xbnds:long_name = "Longitude bounds of 1D network edge (i.e. begin and end longitude)." ; | ||
| Mesh1_edge_xbnds:units = "degrees_east" ; | ||
| double Mesh1_edge_ybnds(nMesh1_edge,Two) ; | ||
| Mesh1_edge_ybnds:standard_name = "latitude" ; | ||
| Mesh1_edge_ybnds:long_name = "Latitude bounds of 1D network edge (i.e. begin and end latitude)." ; | ||
| Mesh1_edge_ybnds:units = "degrees_north" ; | ||
|
|
||
| float datavar(nMesh1_edge) ; | ||
| datavar:mesh = "Mesh1" ; | ||
| datavar:location = "edge" ; | ||
| } |
84 changes: 84 additions & 0 deletions
84
...ris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| netcdf ex2_2d_triangular { | ||
| dimensions: | ||
| nMesh2_node = 4 ; // nNodes | ||
| nMesh2_edge = 5 ; // nEdges | ||
| nMesh2_face = 2 ; // nFaces | ||
|
|
||
| Two = 2 ; | ||
| Three = 3 ; | ||
|
|
||
| variables: | ||
| // Mesh topology | ||
| integer Mesh2 ; | ||
| Mesh2:cf_role = "mesh_topology" ; | ||
| Mesh2:long_name = "Topology data of 2D unstructured mesh" ; | ||
| Mesh2:topology_dimension = 2 ; | ||
| Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; | ||
| Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; | ||
| Mesh2:face_dimension = "nMesh2_face" ; | ||
| Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; // attribute required if variables will be defined on edges | ||
| Mesh2:edge_dimension = "nMesh2_edge" ; | ||
| Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; // optional attribute (requires edge_node_connectivity) | ||
| Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; // optional attribute | ||
| Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; // optional attribute (requires edge_node_connectivity) | ||
| Mesh2:face_face_connectivity = "Mesh2_face_links" ; // optional attribute | ||
| Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; // optional attribute (requires edge_node_connectivity) | ||
| integer Mesh2_face_nodes(nMesh2_face, Three) ; | ||
| Mesh2_face_nodes:cf_role = "face_node_connectivity" ; | ||
| Mesh2_face_nodes:long_name = "Maps every triangular face to its three corner nodes." ; | ||
| Mesh2_face_nodes:start_index = 1 ; | ||
| integer Mesh2_edge_nodes(nMesh2_edge, Two) ; | ||
| Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; | ||
| Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; | ||
| Mesh2_edge_nodes:start_index = 1 ; | ||
|
|
||
| // Optional mesh topology variables | ||
| integer Mesh2_face_edges(nMesh2_face, Three) ; | ||
| Mesh2_face_edges:cf_role = "face_edge_connectivity" ; | ||
| Mesh2_face_edges:long_name = "Maps every triangular face to its three edges." ; | ||
| Mesh2_face_edges:start_index = 1 ; | ||
| integer Mesh2_face_links(nMesh2_face, Three) ; | ||
| Mesh2_face_links:cf_role = "face_face_connectivity" ; | ||
| Mesh2_face_links:long_name = "neighbor faces for faces" ; | ||
| Mesh2_face_links:start_index = 1 ; | ||
| Mesh2_face_links:_FillValue = -999 ; | ||
| Mesh2_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; | ||
| integer Mesh2_edge_face_links(nMesh2_edge, Two) ; | ||
| Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; | ||
| Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; | ||
| Mesh2_edge_face_links:start_index = 1 ; | ||
| Mesh2_edge_face_links:_FillValue = -999 ; | ||
| Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; | ||
|
|
||
| // Mesh node coordinates | ||
| double Mesh2_node_x(nMesh2_node) ; | ||
| Mesh2_node_x:standard_name = "longitude" ; | ||
| Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; | ||
| Mesh2_node_x:units = "degrees_east" ; | ||
| double Mesh2_node_y(nMesh2_node) ; | ||
| Mesh2_node_y:standard_name = "latitude" ; | ||
| Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; | ||
| Mesh2_node_y:units = "degrees_north" ; | ||
|
|
||
| // Optional mesh face and edge coordinate variables | ||
| double Mesh2_face_x(nMesh2_face) ; | ||
| Mesh2_face_x:standard_name = "longitude" ; | ||
| Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh triangle (e.g. circumcenter coordinate)." ; | ||
| Mesh2_face_x:units = "degrees_east" ; | ||
| double Mesh2_face_y(nMesh2_face) ; | ||
| Mesh2_face_y:standard_name = "latitude" ; | ||
| Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh triangle (e.g. circumcenter coordinate)." ; | ||
| Mesh2_face_y:units = "degrees_north" ; | ||
| double Mesh2_edge_x(nMesh2_edge) ; | ||
| Mesh2_edge_x:standard_name = "longitude" ; | ||
| Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; | ||
| Mesh2_edge_x:units = "degrees_east" ; | ||
| double Mesh2_edge_y(nMesh2_edge) ; | ||
| Mesh2_edge_y:standard_name = "latitude" ; | ||
| Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; | ||
| Mesh2_edge_y:units = "degrees_north" ; | ||
|
|
||
| float datavar(nMesh2_face) ; | ||
| datavar:mesh = "Mesh2" ; | ||
| datavar:location = "face" ; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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've just done this for now, as it was irritating me + making debugging hard (not to easily see when there is a mesh or not).
This probably belongs in it's own PR.