Coverage for tests/continuous/test_flip_flop.py: 100%
85 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-02-28 12:51 +1100
« prev ^ index » next coverage.py v7.3.2, created at 2024-02-28 12:51 +1100
1"""
2This module contains tests for scores.continuous.flip_flop
3"""
4import dask
5import numpy as np
6import pytest
7import xarray as xr
9from scores.continuous.flip_flop_impl import (
10 _encompassing_sector_size_np,
11 _flip_flop_index,
12 encompassing_sector_size,
13 flip_flop_index,
14 flip_flop_index_proportion_exceeding,
15)
16from scores.utils import DimensionError
17from tests.assertions import assert_dataarray_equal, assert_dataset_equal
18from tests.continuous import flip_flop_test_data as ntd
21@pytest.mark.parametrize(
22 ("data", "sampling_dim", "is_angular", "expected"),
23 [
24 # 0: 1-D, length=6
25 (ntd.DATA_FFI_1D_6, "letter", False, xr.DataArray(12.5)),
26 # 1: 1-D, with NaN, length=6
27 (ntd.DATA_FFI_1D_6_WITH_NAN, "letter", False, xr.DataArray(np.nan)),
28 # 2: 2-D, with NaN, length=6
29 (
30 ntd.DATA_FFI_2D_6,
31 "letter",
32 False,
33 xr.DataArray([12.5, np.nan], coords=[("number", [1, 2])]),
34 ),
35 # 3: 1-D, length=3, no flip-flops
36 (ntd.DATA_FFI_1D_6_ABC, "letter", False, xr.DataArray(0.0)),
37 # 4: 1-D, length=3
38 (ntd.DATA_FFI_1D_6_BCD, "letter", False, xr.DataArray(10.0)),
39 # 5: 1-D, length=4
40 (ntd.DATA_FFI_1D_6_ACEF, "letter", False, xr.DataArray(25.0)),
41 # 6: 1-D, with NaN, length=3
42 (ntd.DATA_FFI_1D_6_WITH_NAN_ACE, "letter", False, xr.DataArray(np.nan)),
43 # 7: 1-D, length=3, with floating-points
44 (
45 xr.DataArray([np.pi, 0, 2 * np.pi], dims="pi"),
46 "pi",
47 False,
48 xr.DataArray(np.pi),
49 ),
50 # 8: 3-D, length=3, with floating-points
51 (ntd.DATA_FFI_3D, "letter", False, ntd.EXP_FFI_CASE8),
52 # 9: 3-D, length=3, with floating-points, directional data
53 (ntd.DATA_FFI_3D_DIR, "letter", True, ntd.EXP_FFI_CASE9),
54 ],
55)
56def test__flip_flop_index(data, sampling_dim, is_angular, expected):
57 """Tests that _flip_flop_index returns the correct object"""
58 calculated = _flip_flop_index(data, sampling_dim, is_angular=is_angular)
59 assert_dataarray_equal(calculated, expected, decimals=8)
62def test__flip_flop_index_smoke_raises():
63 """Smoke tests that _flip_flop_index raises the correct exception"""
64 with pytest.raises(DimensionError) as ex:
65 _flip_flop_index(ntd.DATA_FFI_1D_6, "banana")
66 assert "not superset to the dimensions ['banana']" in str(ex.value)
69@pytest.mark.parametrize(
70 ("data", "sampling_dim", "is_angular", "expected"),
71 [
72 # 0: 1-D, selections={}
73 (ntd.DATA_FFI_1D_6, "letter", False, ntd.EXP_FFI_SUB_CASE0),
74 # 1: 1-D with NaN, selections={}
75 (ntd.DATA_FFI_1D_6_WITH_NAN, "letter", False, ntd.EXP_FFI_SUB_CASE1),
76 # 2: 2-D, selections={}
77 (ntd.DATA_FFI_2D_6, "letter", False, ntd.EXP_FFI_SUB_CASE2),
78 # 3: 1-D with float values, selections = {}
79 (
80 xr.DataArray([np.pi, 0, 2 * np.pi], dims="pi"),
81 "pi",
82 False,
83 ntd.EXP_FFI_SUB_CASE3,
84 ),
85 # 4: 2-D, is_angular=True, selections={}
86 (ntd.DATA_FFI_2D_6, "letter", True, ntd.EXP_FFI_SUB_CASE2),
87 ],
88)
89def test_flip_flop_index_no_selections(data, sampling_dim, is_angular, expected):
90 """
91 Tests that flip_flop_index returns the correct result when
92 **selections are not supplied
93 """
94 calculated = flip_flop_index(data, sampling_dim, is_angular=is_angular)
95 assert_dataarray_equal(calculated, expected, decimals=8)
98@pytest.mark.parametrize(
99 ("data", "sampling_dim", "is_angular", "selections", "expected"),
100 [
101 # 0: 1-D, selections pulling out in different order
102 (
103 ntd.DATA_FFI_1D_6,
104 "letter",
105 False,
106 {"back_to_front": ["e", "a", "c"]},
107 ntd.EXP_FFI_SUB_CASE4,
108 ),
109 # 1: 2-D, selections specified (length of samples = 3)
110 (
111 ntd.DATA_FFI_2D_6,
112 "letter",
113 False,
114 ntd.DICT_FFI_SUB_CASE4,
115 ntd.EXP_FFI_SUB_CASE5,
116 ),
117 # 2: 2-D, selections specified (length of samples = 4)
118 (
119 ntd.DATA_FFI_2D_6,
120 "letter",
121 False,
122 {"banana": ["a", "c", "e", "f"]},
123 ntd.EXP_FFI_SUB_CASE6,
124 ),
125 # 3: 3-D, selections specified (length of samples = 3)
126 (
127 ntd.DATA_FFI_3D,
128 "letter",
129 False,
130 {"banana": ["a", "b", "c"]},
131 ntd.EXP_FFI_SUB_CASE7,
132 ),
133 # 4: 1-D with floats coordinates
134 (
135 xr.DataArray([np.pi, 0, 2 * np.pi], dims=["pi"], coords={"pi": [3.14, np.pi, np.e]}),
136 "pi",
137 False,
138 {"irrational": [np.e, np.pi, 3.14]},
139 ntd.EXP_FFI_SUB_CASE8,
140 ),
141 # 5: 3-D
142 # SPOT-CHECKED by DG
143 (
144 ntd.DATA_FFI_2X2X4,
145 "int",
146 False,
147 {"one": [1, 2, 3], "two": [2, 3, 4]},
148 ntd.EXP_FFI_SUB_CASE9,
149 ),
150 # 10: 2-D, selections specified with different length samples
151 (
152 ntd.DATA_FFI_2D_6,
153 "letter",
154 False,
155 {"3letters": ["a", "c", "e"], "4letters": ["a", "c", "e", "f"]},
156 ntd.EXP_FFI_SUB_CASE10,
157 ),
158 # 11: 2-D, selections specified with different length samples, angular data
159 (
160 ntd.DATA_FFI_2D_6,
161 "letter",
162 True,
163 {"3letters": ["a", "c", "e"], "4letters": ["a", "c", "e", "f"]},
164 ntd.EXP_FFI_SUB_CASE10,
165 ),
166 ],
167)
168def test_flip_flop_index(data, sampling_dim, is_angular, selections, expected):
169 """
170 Tests that flip_flop_index returns the correct result when
171 **selections are supplied
172 """
173 calculated = flip_flop_index(data, sampling_dim, is_angular=is_angular, **selections)
174 assert_dataset_equal(calculated, expected, decimals=8)
177def test_flip_flop_index_raises():
178 """
179 Test that flip_flop_index raises the correct exception when an invalid
180 selection is supplied
181 """
182 with pytest.raises(KeyError) as ex:
183 flip_flop_index(ntd.DATA_FFI_1D_6, "letter", zero=["a", "e", "g"])
184 assert "for `selections` item {'zero': ['a', 'e', 'g']}, not all values" in str(ex.value)
187@pytest.mark.parametrize(
188 ("data", "dims", "skipna", "expected"),
189 [
190 (ntd.ESS, ["i", "j"], False, ntd.EXP_ESS_DIM_K),
191 (ntd.ESS, ["i", "j"], True, ntd.EXP_ESS_DIM_K_SKIPNA),
192 (ntd.ESS.sel(j=[1]), ["i", "j"], False, ntd.EXP_ESS_DIM_K_1J),
193 ],
194)
195def test_encompassing_sector_size(data, dims, skipna, expected):
196 """
197 Tests encompassing_sector_size
198 """
199 calculated = encompassing_sector_size(data, dims, skipna=skipna)
200 assert_dataarray_equal(calculated, expected, decimals=7)
203@pytest.mark.parametrize(
204 ("angles", "expected"),
205 ntd.ENC_SIZE_NP_TESTS_SKIPNA,
206)
207def test_encompassing_sector_size_np_1d_skipna(angles, expected):
208 """test _encompassing_sector_size_np, 1d test cases with skipna=True"""
209 angles = np.array(angles)
210 result = _encompassing_sector_size_np(angles, skipna=True)
211 assert np.allclose(result, expected, equal_nan=True)
214@pytest.mark.parametrize(
215 ("angles", "expected"),
216 ntd.ENC_SIZE_NP_TESTS_SKIPNA,
217)
218def test_encompassing_sector_size_np_1d_skipna_noorder(angles, expected):
219 """
220 test _encompassing_sector_size_np
221 1d test cases, skipna=True, reverse test case order to make sure
222 implementation not dependent on ordering of angles.
223 """
224 angles = np.array(list(reversed(angles)))
225 result = _encompassing_sector_size_np(angles, skipna=True)
226 assert np.allclose(result, expected, equal_nan=True)
229@pytest.mark.parametrize(
230 ("angles", "expected"),
231 ntd.ENC_SIZE_NP_TESTS_SKIPNA,
232)
233def test_encompassing_sector_size_np_1d_skipna_offsets(angles, expected):
234 """
235 test _encompassing_sector_size_np
236 1d test cases, skipna=True, with various offsets applied, to test
237 check implementation works where angles aren't all in (0, 360) range and
238 even if angles span 0/360/720 boundaries.
239 """
240 angles = np.array(angles)
241 for offset in range(-720, 720, 90):
242 testcase = angles + offset
243 result = _encompassing_sector_size_np(testcase, skipna=True)
244 assert np.allclose(result, expected, equal_nan=True)
247@pytest.mark.parametrize(
248 ("angles", "expected"),
249 ntd.ENC_SIZE_NP_TESTS_NOSKIPNA,
250)
251def test_encompassing_sector_size_np_1d_noskipna(angles, expected):
252 """test _encompassing_sector_size_np, 1d test cases, skipna=True"""
253 angles = np.array(angles)
254 result = _encompassing_sector_size_np(angles, skipna=False)
255 assert np.allclose(result, expected, equal_nan=True)
258@pytest.mark.parametrize(
259 ("angles", "axis_to_collapse", "skipna", "expected"),
260 [
261 (ntd.ENC_SIZE_3D_TEST_AXIS0, 0, True, ntd.ENC_SIZE_3D_ANSW_AXIS0_SKIPNA),
262 (ntd.ENC_SIZE_3D_TEST_AXIS0, 0, False, ntd.ENC_SIZE_3D_ANSW_AXIS0_NOSKIPNA),
263 (ntd.ENC_SIZE_3D_TEST_AXIS2, 2, True, ntd.ENC_SIZE_3D_ANSW_AXIS2_SKIPNA),
264 (ntd.ENC_SIZE_3D_TEST_AXIS2, 2, False, ntd.ENC_SIZE_3D_ANSW_AXIS2_NOSKIPNA),
265 ],
266)
267def test_encompassing_sector_size_np_3d(angles, axis_to_collapse, skipna, expected):
268 """
269 Test _encompassing_sector_size_np with 3d test cases.
270 """
271 result = _encompassing_sector_size_np(angles, axis_to_collapse=axis_to_collapse, skipna=skipna)
272 assert np.allclose(result, expected, equal_nan=True)
275@pytest.mark.parametrize(
276 ("data", "dims", "skipna"),
277 [
278 (ntd.ESS, ["i"], False),
279 (ntd.ESS, ["j"], True),
280 (ntd.ESS, ["foobar"], True),
281 ],
282)
283def test_encompassing_sector_size_raises(data, dims, skipna):
284 """
285 Tests encompassing_sector_size for cases where:
286 * Too many dimensions reduced for skipna true and false
287 * Dimension not in existing dataset
288 """
289 with pytest.raises(DimensionError):
290 encompassing_sector_size(data, dims, skipna=skipna)
293@pytest.mark.parametrize(
294 (
295 "data",
296 "sampling_dim",
297 "thresholds",
298 "is_angular",
299 "selections",
300 "reduce_dims",
301 "preserve_dims",
302 "expected",
303 ),
304 [
305 # 0. 3-D, preserve_dims=None, reduce_dims=None
306 (
307 ntd.DATA_FFI_2X2X4,
308 "int",
309 [0, 1, 5],
310 False,
311 {"one": [1, 2, 3], "two": [2, 3, 4]},
312 None,
313 None,
314 ntd.EXP_FFI_PE_NONE,
315 ),
316 # 1. 3-D, preserve_dims=['char']
317 (
318 ntd.DATA_FFI_2X2X4,
319 "int",
320 [0, 1, 5],
321 False,
322 {"one": [1, 2, 3], "two": [2, 3, 4]},
323 None,
324 ["char"],
325 ntd.EXP_FFI_PE_CHAR,
326 ),
327 # 2. 3-D, preserve_dims=['char', 'bool']
328 (
329 ntd.DATA_FFI_2X2X4,
330 "int",
331 [0, 1, 5],
332 False,
333 {"one": [1, 2, 3], "two": [2, 3, 4]},
334 None,
335 ["char", "bool"],
336 ntd.EXP_FFI_PE_CHARBOOL,
337 ),
338 # 3. 3-D, preserve_dims=['char', 'bool'], angular data
339 (
340 ntd.DATA_FFI_2X2X4_DIR,
341 "int",
342 [0, 50, 100],
343 True,
344 {"one": [1, 2, 3], "two": [2, 3, 4]},
345 None,
346 ["char", "bool"],
347 ntd.EXP_FFI_PE_CHARBOOL_DIR,
348 ),
349 # 4. 3-D, reduce_dims=['bool']
350 (
351 ntd.DATA_FFI_2X2X4,
352 "int",
353 [0, 1, 5],
354 False,
355 {"one": [1, 2, 3], "two": [2, 3, 4]},
356 ["bool"],
357 None,
358 ntd.EXP_FFI_PE_CHAR,
359 ),
360 ],
361)
362def test_flip_flop_index_proportion_exceeding(
363 data, sampling_dim, thresholds, is_angular, selections, reduce_dims, preserve_dims, expected
364):
365 """
366 Tests that flip_flop_index_proportion_exceeding returns the correct object
367 """
368 calculated = flip_flop_index_proportion_exceeding(
369 data,
370 sampling_dim,
371 thresholds,
372 is_angular=is_angular,
373 reduce_dims=reduce_dims,
374 preserve_dims=preserve_dims,
375 **selections,
376 )
377 assert_dataset_equal(calculated, expected, decimals=8)
380@pytest.mark.parametrize(
381 "dims_kwargs",
382 [{"preserve_dims": ["letter", "banana"]}, {"reduce_dims": ["letter", "banana"]}],
383 ids=["preserve_dims", "reduce_dims"],
384)
385def test_flip_flop_index_proportion_exceeding_raises(dims_kwargs):
386 """
387 Smoke tests that flip_flop_index_proportion_exceeding raises the correct
388 exception when `sampling_dim` is in `dims`
389 """
390 with pytest.raises(DimensionError) as ex:
391 flip_flop_index_proportion_exceeding(
392 xr.DataArray([[1]], dims=["letter", "banana"]),
393 "letter",
394 [10, 20],
395 **dims_kwargs,
396 )
397 assert "`sampling_dim`: 'letter' must not be in dimensions to " in str(ex.value)
400def test_flip_flop_index_is_dask_compatible():
401 """
402 Test that flip flop works with dask.
403 """
404 dask_array = ntd.DATA_FFI_1D_6.chunk()
405 result = flip_flop_index(dask_array, "letter")
406 assert isinstance(result.data, dask.array.Array)
407 result = result.compute()
408 xr.testing.assert_equal(result, ntd.EXP_FFI_SUB_CASE0)
409 assert isinstance(result.data, np.ndarray)
412def test_flip_flop_index_proportion_exceeding_is_dask_compatible():
413 """
414 Test that flip flop proportion exceeding works with dask.
415 """
416 dask_array = ntd.DATA_FFI_2X2X4.chunk()
417 result = flip_flop_index_proportion_exceeding(dask_array, "int", [0, 1, 5], **{"one": [1, 2, 3], "two": [2, 3, 4]})
418 assert isinstance(result.data_vars["one"].data, dask.array.Array)
419 result = result.compute()
420 xr.testing.assert_equal(result, ntd.EXP_FFI_PE_NONE)
421 assert isinstance(result.one.data, np.ndarray)