Skip to content
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

Mesh full comparison #4439

Merged
merged 15 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from 7 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
11 changes: 8 additions & 3 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ This document explains the changes made to Iris for this release

#. `@bjlittle`_, `@pp-mo`_ and `@trexfeathers`_ added support for unstructured
meshes, as described by `UGRID`_. This involved adding a data model (:pull:`3968`,
:pull:`4014`, :pull:`4027`, :pull:`4036`, :pull:`4053`) and API (:pull:`4063`,
:pull:`4064`), and supporting representation (:pull:`4033`, :pull:`4054`) of
data on meshes.
:pull:`4014`, :pull:`4027`, :pull:`4036`, :pull:`4053`, :pull:`4439`) and
API (:pull:`4063`, :pull:`4064`), and supporting representation
(:pull:`4033`, :pull:`4054`) of data on meshes.
Most of this new API can be found in :mod:`iris.experimental.ugrid`. The key
objects introduced are :class:`iris.experimental.ugrid.mesh.Mesh`,
:class:`iris.experimental.ugrid.mesh.MeshCoord` and
Expand Down Expand Up @@ -127,6 +127,11 @@ This document explains the changes made to Iris for this release
data to take significantly longer than with real data. Relevant benchmark
shows a time decrease from >10s to 625ms. (:issue:`4280`, :pull:`4400`)

#. `@trexfeathers`_ changed :class:`~iris.coords._DimensionalMetadata` and
:class:`~iris.experimental.ugrid.Connectivity` equality methods to preserve
array laziness, allowing efficient comparisons even with larger-than-memory
objects.


💣 Incompatible Changes
=======================
Expand Down
10 changes: 5 additions & 5 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,27 +343,27 @@ def __repr__(self):

def __eq__(self, other):
# Note: this method includes bounds handling code, but it only runs
# within Coord type instances, as only these allow bounds to be set.
# within Coord type instances, as only these allow bounds to be set.

eq = NotImplemented
# If the other object has a means of getting its definition, then do
# the comparison, otherwise return a NotImplemented to let Python try
# to resolve the operator elsewhere.
# the comparison, otherwise return a NotImplemented to let Python try
# to resolve the operator elsewhere.
if hasattr(other, "metadata"):
# metadata comparison
eq = self.metadata == other.metadata
# data values comparison
if eq and eq is not NotImplemented:
eq = iris.util.array_equal(
self._values, other._values, withnans=True
self._core_values(), other._core_values(), withnans=True
)

# Also consider bounds, if we have them.
# (N.B. though only Coords can ever actually *have* bounds).
if eq and eq is not NotImplemented:
if self.has_bounds() and other.has_bounds():
eq = iris.util.array_equal(
self.bounds, other.bounds, withnans=True
self.core_bounds(), other.core_bounds(), withnans=True
)
else:
eq = not self.has_bounds() and not other.has_bounds()
Expand Down
2 changes: 2 additions & 0 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -3507,6 +3507,8 @@ def __eq__(self, other):

# Having checked everything else, check approximate data equality.
if result:
# TODO: why do we use allclose() here, but strict equality in
# _DimensionalMetadata (via util.array_equal())?
result = da.allclose(
self.core_data(), other.core_data()
).compute()
Expand Down
33 changes: 23 additions & 10 deletions lib/iris/experimental/ugrid/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from ...config import get_logger
from ...coords import AuxCoord, _DimensionalMetadata
from ...exceptions import ConnectivityNotFoundError, CoordinateNotFoundError
from ...util import guess_coord_axis
from ...util import array_equal, guess_coord_axis
from .metadata import ConnectivityMetadata, MeshCoordMetadata, MeshMetadata

# Configure the logger.
Expand Down Expand Up @@ -483,12 +483,19 @@ def __eq__(self, other):
if hasattr(other, "metadata"):
# metadata comparison
eq = self.metadata == other.metadata
if eq:
eq = self.shape == other.shape
if eq:
eq = (
self.indices_by_src() == other.indices_by_src()
).all()
self.shape == other.shape
and self.src_dim == other.src_dim
) or (
self.shape == other.shape[::-1]
and self.src_dim == other.tgt_dim
)
if eq:
eq = array_equal(
self.indices_by_src(self.core_indices()),
other.indices_by_src(other.core_indices()),
)
return eq

def transpose(self):
Expand Down Expand Up @@ -939,8 +946,16 @@ def axes_assign(coord_list):
return cls(**mesh_kwargs)

def __eq__(self, other):
# TBD: this is a minimalist implementation and requires to be revisited
return id(self) == id(other)
result = NotImplemented

if isinstance(other, Mesh):
result = self.metadata == other.metadata
if result:
result = self.all_coords == other.all_coords
if result:
result = self.all_connectivities == other.all_connectivities

return result

def __hash__(self):
# Allow use in sets and as dictionary keys, as is done for :class:`iris.cube.Cube`.
Expand Down Expand Up @@ -2883,9 +2898,7 @@ def copy(self, points=None, bounds=None):
"""
# Override Coord.copy, so that we can ensure it does not duplicate the
# Mesh object (via deepcopy).
# This avoids copying Meshes. It is also required to allow a copied
# MeshCoord to be == the original, since for now Mesh == is only true
# for the same identical object.
# This avoids copying Meshes.

# FOR NOW: also disallow changing points/bounds at all.
if points is not None or bounds is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def setUp(self):
# Crete an instance, with non-default arguments to allow testing of
# correct property setting.
self.kwargs = {
"indices": np.linspace(1, 9, 9, dtype=int).reshape((3, -1)),
"indices": np.linspace(1, 12, 12, dtype=int).reshape((4, -1)),
"cf_role": "face_node_connectivity",
"long_name": "my_face_nodes",
"var_name": "face_nodes",
Expand Down Expand Up @@ -91,7 +91,7 @@ def test_lazy_src_lengths(self):
self.assertTrue(is_lazy_data(self.connectivity.lazy_src_lengths()))

def test_src_lengths(self):
expected = [3, 3, 3]
expected = [4, 4, 4]
self.assertArrayEqual(expected, self.connectivity.src_lengths())

def test___str__(self):
Expand All @@ -102,7 +102,7 @@ def test___str__(self):

def test___repr__(self):
expected = (
"Connectivity(array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), "
"Connectivity(array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]), "
"cf_role='face_node_connectivity', long_name='my_face_nodes', "
"var_name='face_nodes', attributes={'notes': 'this is a test'}, "
"start_index=1, src_dim=1)"
Expand All @@ -122,7 +122,7 @@ def test___eq__(self):
equivalent_kwargs["src_dim"] = 1 - self.kwargs["src_dim"]
equivalent = Connectivity(**equivalent_kwargs)
self.assertFalse(
(equivalent.indices == self.connectivity.indices).all()
np.array_equal(equivalent.indices, self.connectivity.indices)
)
self.assertEqual(equivalent, self.connectivity)

Expand Down
29 changes: 28 additions & 1 deletion lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def setUpClass(cls):
cls.kwargs = {
"topology_dimension": 1,
"node_coords_and_axes": ((cls.NODE_LON, "x"), (cls.NODE_LAT, "y")),
"connectivities": cls.EDGE_NODE,
"connectivities": [cls.EDGE_NODE],
"long_name": "my_topology_mesh",
"var_name": "mesh",
"attributes": {"notes": "this is a test"},
Expand Down Expand Up @@ -124,6 +124,33 @@ def test___repr__(self):
)
self.assertEqual(expected, self.mesh.__repr__())

def test___eq__(self):
# The dimension names do not participate in equality.
equivalent_kwargs = self.kwargs.copy()
equivalent_kwargs["node_dimension"] = "something_else"
equivalent = mesh.Mesh(**equivalent_kwargs)
self.assertEqual(equivalent, self.mesh)

def test_different(self):
# 2021-11-26 currently not possible to have a metadata-only difference
# - only topology_dimension makes a difference and that also mandates
# different connectivities.
stephenworsley marked this conversation as resolved.
Show resolved Hide resolved

different_kwargs = self.kwargs.copy()
ncaa = self.kwargs["node_coords_and_axes"]
new_lat = ncaa[1][0].copy(points=ncaa[1][0].points + 1)
new_ncaa = (ncaa[0], (new_lat, "y"))
different_kwargs["node_coords_and_axes"] = new_ncaa
different = mesh.Mesh(**different_kwargs)
self.assertNotEqual(different, self.mesh)

different_kwargs = self.kwargs.copy()
conns = self.kwargs["connectivities"]
new_conn = conns[0].copy(conns[0].indices + 1)
different_kwargs["connectivities"] = new_conn
different = mesh.Mesh(**different_kwargs)
self.assertNotEqual(different, self.mesh)

def test_all_connectivities(self):
expected = mesh.Mesh1DConnectivities(self.EDGE_NODE)
self.assertEqual(expected, self.mesh.all_connectivities)
Expand Down
11 changes: 9 additions & 2 deletions lib/iris/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ def array_equal(array1, array2, withnans=False):
Args:

* array1, array2 (arraylike):
args to be compared, after normalising with :func:`np.asarray`.
args to be compared, normalised if necessary with :func:`np.asarray`.

Kwargs:

Expand All @@ -360,7 +360,14 @@ def array_equal(array1, array2, withnans=False):
with additional support for arrays of strings and NaN-tolerant operation.

"""
array1, array2 = np.asarray(array1), np.asarray(array2)

def normalise_array(array):
if not is_lazy_data(array):
array = np.asarray(array)
# (All other np operations in array_equal() preserve array laziness).
return array

array1, array2 = normalise_array(array1), normalise_array(array2)

eq = array1.shape == array2.shape
if eq:
Expand Down