Skip to content

Commit 09805c5

Browse files
authored
Merge e43997d into 9f83a9a
2 parents 9f83a9a + e43997d commit 09805c5

File tree

5 files changed

+302
-143
lines changed

5 files changed

+302
-143
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: 176 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -4,133 +4,184 @@
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+
np.array([1, 2]),
72+
ma.masked_array([1, 3], mask=[0, 1]),
73+
False,
74+
False,
75+
),
76+
# test masked/unmasked_equivalence
77+
(
78+
np.array([1, 2, 2]),
79+
ma.masked_array([1, 2, 2]),
80+
False,
81+
True,
82+
),
83+
(
84+
np.array([1, 2, 2]),
85+
ma.masked_array([1, 2, 2], mask=[0, 0, 0]),
86+
False,
87+
True,
88+
),
89+
# test fully masked arrays
90+
(
91+
ma.masked_array(np.arange(24).reshape(2, 3, 4), mask=True),
92+
ma.masked_array(np.arange(24).reshape(2, 3, 4), mask=True),
93+
False,
94+
True,
95+
),
96+
# test fully masked 0d arrays
97+
(
98+
ma.masked_array(3, mask=True),
99+
ma.masked_array(3, mask=True),
100+
False,
101+
True,
102+
),
103+
# test fully masked string arrays
104+
(
105+
ma.masked_array(["a", "b", "c"], mask=True),
106+
ma.masked_array(["a", "b", "c"], mask=[1, 1, 1]),
107+
False,
108+
True,
109+
),
110+
# test partially masked string arrays
111+
(
112+
ma.masked_array(["a", "b", "c"], mask=[1, 0, 1]),
113+
ma.masked_array(["a", "b", "c"], mask=[1, 0, 1]),
114+
False,
115+
True,
116+
),
117+
# test string arrays equal
118+
(
119+
np.array(["abc", "def", "efg"]),
120+
np.array(["abc", "def", "efg"]),
121+
False,
122+
True,
123+
),
124+
# test string arrays different contents
125+
(
126+
np.array(["abc", "def", "efg"]),
127+
np.array(["abc", "de", "efg"]),
128+
False,
129+
False,
130+
),
131+
# test string arrays subset
132+
(
133+
np.array(["abc", "def", "efg"]),
134+
np.array(["abc", "def"]),
135+
False,
136+
False,
137+
),
138+
(
139+
np.array(["abc", "def"]),
140+
np.array(["abc", "def", "efg"]),
141+
False,
142+
False,
143+
),
144+
# test string arrays unequal dimensionality
145+
(np.array("abc"), np.array(["abc"]), False, False),
146+
(np.array(["abc"]), np.array("abc"), False, False),
147+
(np.array("abc"), np.array([["abc"]]), False, False),
148+
(np.array(["abc"]), np.array([["abc"]]), False, False),
149+
# test string arrays 0d and scalar
150+
(np.array("foobar"), "foobar", False, True),
151+
(np.array("foobar"), "foo", False, False),
152+
(np.array("foobar"), "foobar.", False, False),
153+
# test nan equality nan ne nan
154+
(ARRAY2, ARRAY2, False, False),
155+
(ARRAY2, ARRAY2.copy(), False, False),
156+
# test nan equality nan naneq nan
157+
(ARRAY2, ARRAY2, True, True),
158+
(ARRAY2, ARRAY2.copy(), True, True),
159+
# test nan equality nan nanne a
160+
(
161+
np.array([1.0, np.nan, 2.0, np.nan, 3.0]),
162+
np.array([1.0, np.nan, 2.0, 0.0, 3.0]),
163+
True,
164+
False,
165+
),
166+
# test nan equality a nanne b
167+
(
168+
np.array([1.0, np.nan, 2.0, np.nan, 3.0]),
169+
np.array([1.0, np.nan, 2.0, np.nan, 4.0]),
170+
True,
171+
False,
172+
),
173+
]
174+
175+
176+
@pytest.mark.parametrize("lazy", [False, True])
177+
@pytest.mark.parametrize("array_a,array_b,withnans,eq", TEST_CASES)
178+
def test_array_equal(array_a, array_b, withnans, eq, lazy):
179+
if lazy:
180+
identical = array_a is array_b
181+
if isinstance(array_a, np.ndarray):
182+
array_a = da.asarray(array_a, chunks=2)
183+
if isinstance(array_b, np.ndarray):
184+
array_b = da.asarray(array_b, chunks=1)
185+
if identical:
186+
array_b = array_a
187+
assert eq == array_equal(array_a, array_b, withnans=withnans)

0 commit comments

Comments
 (0)