From 9c2c7c2fe57d8640e26b697b6efc187a95edeb6f Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 25 Feb 2019 13:51:09 +0000 Subject: [PATCH 1/4] Allow coords with NaNs in to compare equal. --- .../newfeature_2019-Feb-25_compare_nan.txt | 2 ++ .../newfeature_2019-Feb-25_nancoords.txt | 4 +++ lib/iris/coords.py | 6 ++-- lib/iris/util.py | 28 ++++++++++++++++--- 4 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt create mode 100644 docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt diff --git a/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt b/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt new file mode 100644 index 0000000000..4b2fc5f2d6 --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt @@ -0,0 +1,2 @@ +* :func:`iris.util.array_equal` now has a 'withnans' keyword, which provides + a NaN-tolerant array comparison. diff --git a/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt b/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt new file mode 100644 index 0000000000..8ebe98077f --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt @@ -0,0 +1,4 @@ +* :class:`iris.coords.AuxCoord`_'s can now test as 'equal' even when the points + or bounds arrays contain NaN values, if values are the same at all points. + Previously this would fail, as conventionally "NaN != NaN" in normal + floating-point arithmetic. diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 5519630c5d..732dd60155 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -817,11 +817,13 @@ def __eq__(self, other): eq = self._as_defn() == other._as_defn() # points comparison if eq: - eq = iris.util.array_equal(self.points, other.points) + eq = iris.util.array_equal(self.points, other.points, + withnans=True) # bounds comparison if eq: if self.has_bounds() and other.has_bounds(): - eq = iris.util.array_equal(self.bounds, other.bounds) + eq = iris.util.array_equal(self.bounds, other.bounds, + withnans=True) else: eq = self.bounds is None and other.bounds is None diff --git a/lib/iris/util.py b/lib/iris/util.py index 3ce693ebfc..6526a10de5 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -331,19 +331,39 @@ def rolling_window(a, window=1, step=1, axis=-1): return rw -def array_equal(array1, array2): +def array_equal(array1, array2, withnans=False): """ Returns whether two arrays have the same shape and elements. - This provides the same functionality as :func:`numpy.array_equal` but with - additional support for arrays of strings. + Args: + + * array1, array2 (arraylike): + args to be compared, after normalising with :func:`np.asarray`. + + Kwargs: + + * withnans (bool): + When unset (default), the result is False if either input contains NaN + points. This is the normal floating-point arithmetic result. + When set, return True if inputs contain the same value in all elements, + _including_ any NaN values. + + This provides much the same functionality as :func:`numpy.array_equal`, but + with additional support for arrays of strings and NaN-tolerant operation. """ array1, array2 = np.asarray(array1), np.asarray(array2) if array1.shape != array2.shape: eq = False else: - eq = bool(np.asarray(array1 == array2).all()) + eqs = array1 == array2 + if withnans and (array1.dtype.kind == 'f' or array2.dtype.kind == 'f'): + nans1, nans2 = np.isnan(array1), np.isnan(array2) + if not np.all(nans1 == nans2): + eq = False + else: + eqs[nans1] = True + eq = np.all(eqs) return eq From 8dc38d0efe760a9cefd2f7e1948a7e75d800b9e5 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 26 Feb 2019 15:46:36 +0000 Subject: [PATCH 2/4] Tests for AuxCoord.__eq__ and util.array_equal(withnans=True). --- lib/iris/tests/unit/coords/test_AuxCoord.py | 15 ++++++++++++++ lib/iris/tests/unit/util/test_array_equal.py | 21 +++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/coords/test_AuxCoord.py b/lib/iris/tests/unit/coords/test_AuxCoord.py index 39e5048b14..a1a9b9a80f 100644 --- a/lib/iris/tests/unit/coords/test_AuxCoord.py +++ b/lib/iris/tests/unit/coords/test_AuxCoord.py @@ -624,5 +624,20 @@ def test_preserves_lazy(self): self.assertArrayAllClose(coord.bounds, test_bounds_ft) +class TestEquality(tests.IrisTest): + def test_nanpoints_eq_self(self): + co1 = AuxCoord([1., np.nan, 2.]) + self.assertEqual(co1, co1) + + def test_nanpoints_eq_copy(self): + co1 = AuxCoord([1., np.nan, 2.]) + co2 = co1.copy() + self.assertEqual(co1, co2) + + def test_nanbounds_eq_self(self): + co1 = AuxCoord([15., 25.], bounds=[[14., 16.], [24., np.nan]]) + self.assertEqual(co1, co1) + + if __name__ == '__main__': tests.main() diff --git a/lib/iris/tests/unit/util/test_array_equal.py b/lib/iris/tests/unit/util/test_array_equal.py index 31fd9dd842..ab5404d43d 100644 --- a/lib/iris/tests/unit/util/test_array_equal.py +++ b/lib/iris/tests/unit/util/test_array_equal.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2019, Met Office # # This file is part of Iris. # @@ -115,6 +115,25 @@ def test_string_arrays_0d_and_scalar(self): self.assertFalse(array_equal(array_a, 'foo')) self.assertFalse(array_equal(array_a, 'foobar.')) + def test_nan_equality_nan_ne_nan(self): + array = np.array([1.0, np.nan, 2.0, np.nan, 3.0]) + self.assertFalse(array_equal(array, array)) + + def test_nan_equality_nan_naneq_nan(self): + array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0]) + array_b = np.array([1.0, np.nan, 2.0, np.nan, 3.0]) + self.assertTrue(array_equal(array_a, array_b, withnans=True)) + + def test_nan_equality_nan_nanne_a(self): + array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0]) + array_b = np.array([1.0, np.nan, 2.0, 0.0, 3.0]) + self.assertFalse(array_equal(array_a, array_b, withnans=True)) + + def test_nan_equality_a_nanne_b(self): + array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0]) + array_b = np.array([1.0, np.nan, 2.0, np.nan, 4.0]) + self.assertFalse(array_equal(array_a, array_b, withnans=True)) + if __name__ == '__main__': tests.main() From 5db955d88d8da5f805679738956b5df148508dd2 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Tue, 26 Feb 2019 17:41:45 +0000 Subject: [PATCH 3/4] Move whatsnew contributions into existing whatsnew document. --- docs/iris/src/whatsnew/2.2.rst | 11 ++++++++++- .../newfeature_2019-Feb-25_compare_nan.txt | 2 -- .../newfeature_2019-Feb-25_nancoords.txt | 4 ---- 3 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt delete mode 100644 docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 11ace1addc..6a9c863a72 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -66,6 +66,9 @@ Iris 2.2 Features discontiguous points in coordinates can be explicitly masked using another new feature :func:`iris.util.mask_cube`. +* :func:`iris.util.array_equal` now has a 'withnans' keyword, which provides + a NaN-tolerant array comparison. + Iris 2.2 Dependency updates ============================= @@ -95,10 +98,16 @@ Bugs fixed in v2.2.1 * Iris can now correctly unpack a column of header objects when saving a pandas DataFrame to a cube. - + * fixed a bug in :meth:`iris.util.new_axis` : copying the resulting cube resulted in an exception, if it contained an aux-factory. +* :class:`iris.coords.AuxCoord`_'s can now test as 'equal' even when the points + or bounds arrays contain NaN values, if values are the same at all points. + Previously this would fail, as conventionally "NaN != NaN" in normal + floating-point arithmetic. + + Documentation Changes ===================== diff --git a/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt b/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt deleted file mode 100644 index 4b2fc5f2d6..0000000000 --- a/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_compare_nan.txt +++ /dev/null @@ -1,2 +0,0 @@ -* :func:`iris.util.array_equal` now has a 'withnans' keyword, which provides - a NaN-tolerant array comparison. diff --git a/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt b/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt deleted file mode 100644 index 8ebe98077f..0000000000 --- a/docs/iris/src/whatsnew/contributions_2.3.0/newfeature_2019-Feb-25_nancoords.txt +++ /dev/null @@ -1,4 +0,0 @@ -* :class:`iris.coords.AuxCoord`_'s can now test as 'equal' even when the points - or bounds arrays contain NaN values, if values are the same at all points. - Previously this would fail, as conventionally "NaN != NaN" in normal - floating-point arithmetic. From 4e02a0f2fa4bff1a595cd5f7f4a034c2da60a0a1 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Thu, 28 Feb 2019 18:16:37 +0000 Subject: [PATCH 4/4] Better logic in util.array_equal. --- lib/iris/util.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/iris/util.py b/lib/iris/util.py index 6526a10de5..3ecc90130b 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -353,17 +353,20 @@ def array_equal(array1, array2, withnans=False): """ array1, array2 = np.asarray(array1), np.asarray(array2) - if array1.shape != array2.shape: - eq = False - else: - eqs = array1 == array2 + + eq = (array1.shape == array2.shape) + if eq: + eqs = (array1 == array2) + if withnans and (array1.dtype.kind == 'f' or array2.dtype.kind == 'f'): nans1, nans2 = np.isnan(array1), np.isnan(array2) if not np.all(nans1 == nans2): - eq = False + eq = False # simply fail else: - eqs[nans1] = True - eq = np.all(eqs) + eqs[nans1] = True # fix NaNs; check all the others + + if eq: + eq = np.all(eqs) # check equal at all points return eq