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 .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ env:
PIP_CACHE_PACKAGES: "pip setuptools wheel nox pyyaml"
# Conda packages to be installed.
CONDA_CACHE_PACKAGES: "nox pip pyyaml"
# Use specific custom iris source feature branch.
IRIS_SOURCE: "github:mesh-data-model"
Copy link
Member

@bjlittle bjlittle Mar 9, 2021

Choose a reason for hiding this comment

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

@stephenworsley Easy as 🥧

That's really pleasing to see 😄



#
Expand Down
20 changes: 13 additions & 7 deletions esmf_regrid/experimental/unstructured_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
# from numpy import ma

from esmf_regrid.esmf_regridder import GridInfo, Regridder

# from esmf_regrid.experimental.unstructured_regrid import MeshInfo
from esmf_regrid.experimental.unstructured_regrid import MeshInfo


# Taken from PR #26
Expand All @@ -29,9 +28,15 @@ def _get_mesh_and_dim(cube):
pass


def _cube_to_MeshInfo(cube):
def _mesh_to_MeshInfo(mesh):
Copy link
Contributor

Choose a reason for hiding this comment

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

The pros we had looked quite useful. Could we keep a bit of a doc string here?

# Returns a MeshInfo object describing the mesh of the cube.
pass
assert mesh.topology_dimension == 2
meshinfo = MeshInfo(
np.stack([coord.points for coord in mesh.node_coords], axis=-1),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

mesh.face_node_connectivity.indices,
mesh.face_node_connectivity.start_index,
)
return meshinfo


def _cube_to_GridInfo(cube):
Expand Down Expand Up @@ -97,14 +102,15 @@ def _regrid_unstructured_to_rectilinear__prepare(src_mesh_cube, target_grid_cube
# TODO: Record appropriate dimensions (i.e. which dimension the mesh belongs to)

grid_x, grid_y = get_xy_coords(target_grid_cube)
# From src_mesh_cube, fetch the mesh, and the dimension on the cube which that
# mesh belongs to (mesh_dim).
mesh, mesh_dim = _get_mesh_and_dim(src_mesh_cube)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a specific reason _get_mesh_and_dim and _mesh_to_MeshInfo are separate and distinct? I see we return mesh_dim below, but could/should mesh_dim be captured in the MeshInfo object?

Copy link
Member

Choose a reason for hiding this comment

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

As discussed, either rename or just document that mesh_dim is not the "dimension of the mesh".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The way this regridder works, it's possible to initialise a regridder so that it will work with any cube that contains the same mesh (checking that the mesh is actally the same is planned to be added later). Because of this, mesh_dim shouldn't be captured at the MeshInfo level since it is specific to the cube being regrid. You may note that this is thrown away in the regridding class during initialisation https://github.com/SciTools-incubator/iris-esmf-regrid/blob/98a339fa29afb1a19f044f1d0b665b1af463a4fd/esmf_regrid/experimental/unstructured_scheme.py#L169 and reconstructed during the call from the new cube https://github.com/SciTools-incubator/iris-esmf-regrid/blob/98a339fa29afb1a19f044f1d0b665b1af463a4fd/esmf_regrid/experimental/unstructured_scheme.py#L176-L178
This redundancy is due to the prepare and perform functions also being used in the following function
https://github.com/SciTools-incubator/iris-esmf-regrid/blob/98a339fa29afb1a19f044f1d0b665b1af463a4fd/esmf_regrid/experimental/unstructured_scheme.py#L140-L144
which does regridding all at once and does not cache. All this is behaviour inherited from iris.

As for why _get_mesh_and_dim and _mesh_to_MeshInfo should be distinct, during the call, _get_mesh_and_dim serves a different roll (fetching the mesh_dim specific to the cube and fetching the mesh for future checking) and the MeshInfo object has already been created and recorded.


meshinfo = _cube_to_MeshInfo(src_mesh_cube)
meshinfo = _mesh_to_MeshInfo(mesh)
gridinfo = _cube_to_GridInfo(target_grid_cube)

regridder = Regridder(meshinfo, gridinfo)

mesh, mesh_dim = _get_mesh_and_dim(src_mesh_cube)

regrid_info = (mesh, mesh_dim, grid_x, grid_y, regridder)

return regrid_info
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Unit tests for :func:`esmf_regrid.experimental.unstructured_scheme._mesh_to_MeshInfo`."""

from iris.coords import AuxCoord
from iris.experimental.ugrid import Connectivity, Mesh
import numpy as np
from numpy import ma
import scipy.sparse

from esmf_regrid.esmf_regridder import Regridder
from esmf_regrid.experimental.unstructured_scheme import _mesh_to_MeshInfo


def _pyramid_topology_connectivity_array():
"""
Generate the face_node_connectivity array for a topological pyramid.

The mesh described is a topological pyramid in the sense that there
exists a polygonal base (described by the indices [0, 1, 2, 3, 4])
and all other faces are triangles connected to a single node (the node
with index 5).
"""
fnc_array = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a comment in here:
# construct face-node-connectivity (fnc)

Copy link
Member

Choose a reason for hiding this comment

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

Agree, a bit of explanation of what is being made here would be helpful

Copy link
Member

Choose a reason for hiding this comment

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

ASCII diagram possible?!

Copy link
Member

Choose a reason for hiding this comment

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

great! excellent artistic skills 🎨

[0, 1, 2, 3, 4],
[1, 0, 5, -1, -1],
[2, 1, 5, -1, -1],
[3, 2, 5, -1, -1],
[4, 3, 5, -1, -1],
[0, 4, 5, -1, -1],
]
fnc_mask = [
[0, 0, 0, 0, 0],
[0, 0, 0, 1, 1],
[0, 0, 0, 1, 1],
[0, 0, 0, 1, 1],
[0, 0, 0, 1, 1],
[0, 0, 0, 1, 1],
]
fnc_ma = ma.array(fnc_array, mask=fnc_mask)
return fnc_ma


def _example_mesh():
"""Generate a global mesh with a pentagonal pyramid topology."""
# The base of the pyramid is the following pentagon.
#
# 60 0 3
# | \ / |
# 10 | 4 |
# | |
# | |
# -60 1----------2
#
# 120 180 -120
#
# The point of the pyramid is at the coordinate (0, 0).
# The geometry is designed so that a valid ESMF object is only produced when
# the orientation is correct (the face nodes are visited in an anticlockwise
# order). This sensitivity is due to the base of the pyramid being convex.

# Generate face_node_connectivity (fnc).
fnc_ma = _pyramid_topology_connectivity_array()
fnc = Connectivity(
fnc_ma,
cf_role="face_node_connectivity",
start_index=0,
)
lon_values = [120, 120, -120, -120, 180, 0]
lat_values = [60, -60, -60, 60, 10, 0]
lons = AuxCoord(lon_values, standard_name="longitude")
lats = AuxCoord(lat_values, standard_name="latitude")
mesh = Mesh(2, ((lons, "x"), (lats, "y")), fnc)
return mesh
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Polygons are described in such a way that nodes are traversed clockwise. There is still some question about what orientations UGRID allows and what orientations ESMF can handle. If UGRID can allow more than ESMF can handle, we may have to catch and handle such cases ourselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Having looked at the UGRID conventions again, the only case in which both clockwise and anticlockwise orientations are allowed is for Fully 3D unstructured meshes. In all other cases, anticlockwise orientation is specified.



def test__mesh_to_MeshInfo():
"""Basic test for :func:`esmf_regrid.experimental.unstructured_scheme._mesh_to_MeshInfo`."""
mesh = _example_mesh()
meshinfo = _mesh_to_MeshInfo(mesh)

expected_nodes = np.array(
[
[120, 60],
[120, -60],
[-120, -60],
[-120, 60],
[180, 10],
[0, 0],
]
)
assert np.array_equal(expected_nodes, meshinfo.node_coords)

expected_connectivity = _pyramid_topology_connectivity_array()
assert np.array_equal(expected_connectivity, meshinfo.fnc)

expected_start_index = 0
assert expected_start_index == meshinfo.esi


def test_anticlockwise_validity():
"""Test validity of objects derived from Mesh objects with anticlockwise orientation."""
mesh = _example_mesh()
meshinfo = _mesh_to_MeshInfo(mesh)

# Ensure conversion to ESMF works without error.
_ = meshinfo.make_esmf_field()

# The following test ensures there are no overlapping cells.
# This catches geometric/topological abnormalities that would arise from,
# for example: switching lat/lon values, using euclidean coords vs spherical.
rg = Regridder(meshinfo, meshinfo)
expected_weights = scipy.sparse.identity(meshinfo.size())
assert np.allclose(expected_weights.todense(), rg.weight_matrix.todense())