Skip to content

Commit 1cacdd6

Browse files
authored
Merge 8217ea9 into 9fede7c
2 parents 9fede7c + 8217ea9 commit 1cacdd6

File tree

5 files changed

+280
-148
lines changed

5 files changed

+280
-148
lines changed

docs/src/whatsnew/latest.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ This document explains the changes made to Iris for this release
6565
older NetCDF formats e.g. ``NETCDF4_CLASSIC`` support a maximum precision of
6666
32-bit. (:issue:`6178`, :pull:`6343`)
6767

68+
#. `@bouweandela`_ fixed handling of masked Dask arrays in
69+
:func:`~iris.util.array_equal`.
70+
6871

6972
💣 Incompatible Changes
7073
=======================

lib/iris/coords.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -589,21 +589,22 @@ def __eq__(self, other):
589589
if hasattr(other, "metadata"):
590590
# metadata comparison
591591
eq = self.metadata == other.metadata
592+
593+
# Also consider bounds, if we have them.
594+
# (N.B. though only Coords can ever actually *have* bounds).
595+
if eq and eq is not NotImplemented:
596+
eq = self.has_bounds() is other.has_bounds()
597+
592598
# data values comparison
593599
if eq and eq is not NotImplemented:
594600
eq = iris.util.array_equal(
595601
self._core_values(), other._core_values(), withnans=True
596602
)
597-
598-
# Also consider bounds, if we have them.
599-
# (N.B. though only Coords can ever actually *have* bounds).
600603
if eq and eq is not NotImplemented:
601604
if self.has_bounds() and other.has_bounds():
602605
eq = iris.util.array_equal(
603606
self.core_bounds(), other.core_bounds(), withnans=True
604607
)
605-
else:
606-
eq = not self.has_bounds() and not other.has_bounds()
607608

608609
return eq
609610

lib/iris/tests/unit/concatenate/test_hashing.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import pytest
1010

1111
from iris import _concatenate
12+
from iris.tests.unit.util.test_array_equal import TEST_CASES
13+
from iris.util import array_equal
1214

1315

1416
@pytest.mark.parametrize(
@@ -75,6 +77,20 @@ def test_compute_hashes(a, b, eq):
7577
assert eq == (hashes["a"] == hashes["b"])
7678

7779

80+
@pytest.mark.parametrize(
81+
"a,b",
82+
[
83+
(a, b)
84+
for (a, b, withnans, eq) in TEST_CASES
85+
if isinstance(a, np.ndarray | da.Array) and isinstance(b, np.ndarray | da.Array)
86+
],
87+
)
88+
def test_compute_hashes_vs_array_equal(a, b):
89+
"""Test that hashing give the same answer as `array_equal(withnans=True)`."""
90+
hashes = _concatenate._compute_hashes({"a": a, "b": b})
91+
assert array_equal(a, b, withnans=True) == (hashes["a"] == hashes["b"])
92+
93+
7894
def test_arrayhash_equal_incompatible_chunks_raises():
7995
hash1 = _concatenate._ArrayHash(1, chunks=((1, 1),))
8096
hash2 = _concatenate._ArrayHash(1, chunks=((2,),))

lib/iris/tests/unit/util/test_array_equal.py

Lines changed: 182 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -4,133 +4,190 @@
44
# See LICENSE in the root of the repository for full licensing details.
55
"""Test function :func:`iris.util.array_equal`."""
66

7+
import dask.array as da
78
import numpy as np
89
import numpy.ma as ma
10+
import pytest
911

1012
from iris.util import array_equal
1113

12-
13-
class Test:
14-
def test_0d(self):
15-
array_a = np.array(23)
16-
array_b = np.array(23)
17-
array_c = np.array(7)
18-
assert array_equal(array_a, array_b)
19-
assert not array_equal(array_a, array_c)
20-
21-
def test_0d_and_scalar(self):
22-
array_a = np.array(23)
23-
assert array_equal(array_a, 23)
24-
assert not array_equal(array_a, 45)
25-
26-
def test_1d_and_sequences(self):
27-
for sequence_type in (list, tuple):
28-
seq_a = sequence_type([1, 2, 3])
29-
array_a = np.array(seq_a)
30-
assert array_equal(array_a, seq_a)
31-
assert not array_equal(array_a, seq_a[:-1])
32-
array_a[1] = 45
33-
assert not array_equal(array_a, seq_a)
34-
35-
def test_nd(self):
36-
array_a = np.array(np.arange(24).reshape(2, 3, 4))
37-
array_b = np.array(np.arange(24).reshape(2, 3, 4))
38-
array_c = np.array(np.arange(24).reshape(2, 3, 4))
39-
array_c[0, 1, 2] = 100
40-
assert array_equal(array_a, array_b)
41-
assert not array_equal(array_a, array_c)
42-
43-
def test_masked_is_not_ignored(self):
44-
array_a = ma.masked_array([1, 2, 3], mask=[1, 0, 1])
45-
array_b = ma.masked_array([2, 2, 2], mask=[1, 0, 1])
46-
assert array_equal(array_a, array_b)
47-
48-
def test_masked_is_different(self):
49-
array_a = ma.masked_array([1, 2, 3], mask=[1, 0, 1])
50-
array_b = ma.masked_array([1, 2, 3], mask=[0, 0, 1])
51-
assert not array_equal(array_a, array_b)
52-
53-
def test_masked_isnt_unmasked(self):
54-
array_a = np.array([1, 2, 2])
55-
array_b = ma.masked_array([1, 2, 2], mask=[0, 0, 1])
56-
assert not array_equal(array_a, array_b)
57-
58-
def test_masked_unmasked_equivelance(self):
59-
array_a = np.array([1, 2, 2])
60-
array_b = ma.masked_array([1, 2, 2])
61-
array_c = ma.masked_array([1, 2, 2], mask=[0, 0, 0])
62-
assert array_equal(array_a, array_b)
63-
assert array_equal(array_a, array_c)
64-
65-
def test_fully_masked_arrays(self):
66-
array_a = ma.masked_array(np.arange(24).reshape(2, 3, 4), mask=True)
67-
array_b = ma.masked_array(np.arange(24).reshape(2, 3, 4), mask=True)
68-
assert array_equal(array_a, array_b)
69-
70-
def test_fully_masked_0d_arrays(self):
71-
array_a = ma.masked_array(3, mask=True)
72-
array_b = ma.masked_array(3, mask=True)
73-
assert array_equal(array_a, array_b)
74-
75-
def test_fully_masked_string_arrays(self):
76-
array_a = ma.masked_array(["a", "b", "c"], mask=True)
77-
array_b = ma.masked_array(["a", "b", "c"], mask=[1, 1, 1])
78-
assert array_equal(array_a, array_b)
79-
80-
def test_partially_masked_string_arrays(self):
81-
array_a = ma.masked_array(["a", "b", "c"], mask=[1, 0, 1])
82-
array_b = ma.masked_array(["a", "b", "c"], mask=[1, 0, 1])
83-
assert array_equal(array_a, array_b)
84-
85-
def test_string_arrays_equal(self):
86-
array_a = np.array(["abc", "def", "efg"])
87-
array_b = np.array(["abc", "def", "efg"])
88-
assert array_equal(array_a, array_b)
89-
90-
def test_string_arrays_different_contents(self):
91-
array_a = np.array(["abc", "def", "efg"])
92-
array_b = np.array(["abc", "de", "efg"])
93-
assert not array_equal(array_a, array_b)
94-
95-
def test_string_arrays_subset(self):
96-
array_a = np.array(["abc", "def", "efg"])
97-
array_b = np.array(["abc", "def"])
98-
assert not array_equal(array_a, array_b)
99-
assert not array_equal(array_b, array_a)
100-
101-
def test_string_arrays_unequal_dimensionality(self):
102-
array_a = np.array("abc")
103-
array_b = np.array(["abc"])
104-
array_c = np.array([["abc"]])
105-
assert not array_equal(array_a, array_b)
106-
assert not array_equal(array_b, array_a)
107-
assert not array_equal(array_a, array_c)
108-
assert not array_equal(array_b, array_c)
109-
110-
def test_string_arrays_0d_and_scalar(self):
111-
array_a = np.array("foobar")
112-
assert array_equal(array_a, "foobar")
113-
assert not array_equal(array_a, "foo")
114-
assert not array_equal(array_a, "foobar.")
115-
116-
def test_nan_equality_nan_ne_nan(self):
117-
array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0])
118-
array_b = array_a.copy()
119-
assert not array_equal(array_a, array_a)
120-
assert not array_equal(array_a, array_b)
121-
122-
def test_nan_equality_nan_naneq_nan(self):
123-
array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0])
124-
array_b = np.array([1.0, np.nan, 2.0, np.nan, 3.0])
125-
assert array_equal(array_a, array_a, withnans=True)
126-
assert array_equal(array_a, array_b, withnans=True)
127-
128-
def test_nan_equality_nan_nanne_a(self):
129-
array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0])
130-
array_b = np.array([1.0, np.nan, 2.0, 0.0, 3.0])
131-
assert not array_equal(array_a, array_b, withnans=True)
132-
133-
def test_nan_equality_a_nanne_b(self):
134-
array_a = np.array([1.0, np.nan, 2.0, np.nan, 3.0])
135-
array_b = np.array([1.0, np.nan, 2.0, np.nan, 4.0])
136-
assert not array_equal(array_a, array_b, withnans=True)
14+
ARRAY1 = np.array(np.arange(24).reshape(2, 3, 4))
15+
ARRAY1[0, 1, 2] = 100
16+
17+
ARRAY2 = np.array([1.0, np.nan, 2.0, np.nan, 3.0])
18+
19+
TEST_CASES = [
20+
# test empty
21+
(np.array([]), np.array([]), False, True),
22+
(np.array([]), np.array([], dtype=np.float64), True, True),
23+
# test 0d
24+
(np.array(23), np.array(23), False, True),
25+
(np.array(23), np.array(7), False, False),
26+
# test 0d and scalar
27+
(np.array(23), 23, False, True),
28+
(np.array(23), 45, False, False),
29+
# test 1d and sequences
30+
(np.array([1, 2, 3]), [1, 2, 3], False, True),
31+
(np.array([1, 2, 3]), [1, 2], False, False),
32+
(np.array([1, 45, 3]), [1, 2, 3], False, False),
33+
(np.array([1, 2, 3]), (1, 2, 3), False, True),
34+
(np.array([1, 2, 3]), (1, 2), False, False),
35+
(np.array([1, 45, 3]), (1, 2, 3), False, False),
36+
# test 3d
37+
(
38+
np.array(np.arange(24).reshape(2, 3, 4)),
39+
np.array(np.arange(24).reshape(2, 3, 4)),
40+
False,
41+
True,
42+
),
43+
(
44+
np.array(np.arange(24).reshape(2, 3, 4)),
45+
ARRAY1,
46+
False,
47+
False,
48+
),
49+
# test masked is not ignored
50+
(
51+
ma.masked_array([1, 2, 3], mask=[1, 0, 1]),
52+
ma.masked_array([2, 2, 2], mask=[1, 0, 1]),
53+
False,
54+
True,
55+
),
56+
# test masked is different
57+
(
58+
ma.masked_array([1, 2, 3], mask=[1, 0, 1]),
59+
ma.masked_array([1, 2, 3], mask=[0, 0, 1]),
60+
False,
61+
False,
62+
),
63+
# test masked isn't unmasked
64+
(
65+
np.array([1, 2, 2]),
66+
ma.masked_array([1, 2, 2], mask=[0, 0, 1]),
67+
False,
68+
False,
69+
),
70+
(
71+
ma.masked_array([1, 2, 2], mask=[0, 0, 1]),
72+
ma.masked_array([1, 2, 2]),
73+
False,
74+
False,
75+
),
76+
(
77+
np.array([1, 2]),
78+
ma.masked_array([1, 3], mask=[0, 1]),
79+
False,
80+
False,
81+
),
82+
# test masked/unmasked_equivalence
83+
(
84+
np.array([1, 2, 2]),
85+
ma.masked_array([1, 2, 2]),
86+
False,
87+
True,
88+
),
89+
(
90+
np.array([1, 2, 2]),
91+
ma.masked_array([1, 2, 2], mask=[0, 0, 0]),
92+
False,
93+
True,
94+
),
95+
# test fully masked arrays
96+
(
97+
ma.masked_array(np.arange(24).reshape(2, 3, 4), mask=True),
98+
ma.masked_array(np.arange(24).reshape(2, 3, 4), mask=True),
99+
False,
100+
True,
101+
),
102+
# test fully masked 0d arrays
103+
(
104+
ma.masked_array(3, mask=True),
105+
ma.masked_array(3, mask=True),
106+
False,
107+
True,
108+
),
109+
# test fully masked string arrays
110+
(
111+
ma.masked_array(["a", "b", "c"], mask=True),
112+
ma.masked_array(["a", "b", "c"], mask=[1, 1, 1]),
113+
False,
114+
True,
115+
),
116+
# test partially masked string arrays
117+
(
118+
ma.masked_array(["a", "b", "c"], mask=[1, 0, 1]),
119+
ma.masked_array(["a", "b", "c"], mask=[1, 0, 1]),
120+
False,
121+
True,
122+
),
123+
# test string arrays equal
124+
(
125+
np.array(["abc", "def", "efg"]),
126+
np.array(["abc", "def", "efg"]),
127+
False,
128+
True,
129+
),
130+
# test string arrays different contents
131+
(
132+
np.array(["abc", "def", "efg"]),
133+
np.array(["abc", "de", "efg"]),
134+
False,
135+
False,
136+
),
137+
# test string arrays subset
138+
(
139+
np.array(["abc", "def", "efg"]),
140+
np.array(["abc", "def"]),
141+
False,
142+
False,
143+
),
144+
(
145+
np.array(["abc", "def"]),
146+
np.array(["abc", "def", "efg"]),
147+
False,
148+
False,
149+
),
150+
# test string arrays unequal dimensionality
151+
(np.array("abc"), np.array(["abc"]), False, False),
152+
(np.array(["abc"]), np.array("abc"), False, False),
153+
(np.array("abc"), np.array([["abc"]]), False, False),
154+
(np.array(["abc"]), np.array([["abc"]]), False, False),
155+
# test string arrays 0d and scalar
156+
(np.array("foobar"), "foobar", False, True),
157+
(np.array("foobar"), "foo", False, False),
158+
(np.array("foobar"), "foobar.", False, False),
159+
# test nan equality nan ne nan
160+
(ARRAY2, ARRAY2, False, False),
161+
(ARRAY2, ARRAY2.copy(), False, False),
162+
# test nan equality nan naneq nan
163+
(ARRAY2, ARRAY2, True, True),
164+
(ARRAY2, ARRAY2.copy(), True, True),
165+
# test nan equality nan nanne a
166+
(
167+
np.array([1.0, np.nan, 2.0, np.nan, 3.0]),
168+
np.array([1.0, np.nan, 2.0, 0.0, 3.0]),
169+
True,
170+
False,
171+
),
172+
# test nan equality a nanne b
173+
(
174+
np.array([1.0, np.nan, 2.0, np.nan, 3.0]),
175+
np.array([1.0, np.nan, 2.0, np.nan, 4.0]),
176+
True,
177+
False,
178+
),
179+
]
180+
181+
182+
@pytest.mark.parametrize("lazy", [False, True])
183+
@pytest.mark.parametrize("array_a,array_b,withnans,eq", TEST_CASES)
184+
def test_array_equal(array_a, array_b, withnans, eq, lazy):
185+
if lazy:
186+
identical = array_a is array_b
187+
if isinstance(array_a, np.ndarray):
188+
array_a = da.asarray(array_a, chunks=2)
189+
if isinstance(array_b, np.ndarray):
190+
array_b = da.asarray(array_b, chunks=1)
191+
if identical:
192+
array_b = array_a
193+
assert eq == array_equal(array_a, array_b, withnans=withnans)

0 commit comments

Comments
 (0)