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
11 changes: 10 additions & 1 deletion docs/iris/src/whatsnew/2.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Member

@lbdreyer lbdreyer Feb 28, 2019

Choose a reason for hiding this comment

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

I'm torn with this.
Part of me wants it in under a sub-heading, similar to the "Bugs fixed in 1.7.3" sub headings in the 1.7 what's new but I'm not sure...

Copy link
Member Author

@pp-mo pp-mo Feb 28, 2019

Choose a reason for hiding this comment

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

Hmm. It's awkward because bugfix releases aren't supposed to contain new features !

We could treat the new keyword as a "private matter" for now, and then announce it with enormous fanfare in the nest minor release ??
I think that means putting the whatsnew contribution back into a textfile in a contributions_2.3.0 directory. The actual code changes (+docstring) can stay though,

Would you favour that solution @lbdreyer ?

Copy link
Member

Choose a reason for hiding this comment

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

I believe the outcome of the offline discussion was to keep what you have already done.
Admittedly the reasons for that decision are a little fuzzy we discussed this over a week ago, but I think I personally wanted to not keep it private, but not add confusion by adding a "features added in 2.2.1" section

So basically, I'm happy with this change as it is.


Iris 2.2 Dependency updates
=============================
Expand Down Expand Up @@ -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
=====================
Expand Down
6 changes: 4 additions & 2 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions lib/iris/tests/unit/coords/test_AuxCoord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
21 changes: 20 additions & 1 deletion lib/iris/tests/unit/util/test_array_equal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2014 - 2015, Met Office
# (C) British Crown Copyright 2014 - 2019, Met Office
Copy link
Member

Choose a reason for hiding this comment

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

Did you want to follow your suggested approach and remove the year

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'd rather do it separately.

My reason is, I just realised, we are going to have to modify the licence-header check in test_coding_standards.py first ... see #3285

Copy link
Member

Choose a reason for hiding this comment

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

Yep, that makes sense 👍

#
# This file is part of Iris.
#
Expand Down Expand Up @@ -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()
37 changes: 30 additions & 7 deletions lib/iris/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down