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/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/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() diff --git a/lib/iris/util.py b/lib/iris/util.py index 3ce693ebfc..3ecc90130b 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -331,19 +331,42 @@ 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()) + + 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 # simply fail + else: + eqs[nans1] = True # fix NaNs; check all the others + + if eq: + eq = np.all(eqs) # check equal at all points return eq