Coverage for tests/continuous/test_flip_flop.py: 100%

1"""

2 This module contains tests for scores.continuous.flip_flop

3"""

4 import dask

5 import numpy as np

6 import pytest

7 import xarray as xr

8

9 from 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)

16 from scores.utils import DimensionError

17 from tests.assertions import assert_dataarray_equal, assert_dataset_equal

18 from tests.continuous import flip_flop_test_data as ntd

19

20

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)

56 def 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)

60

61

62 def 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)

67

68

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)

89 def 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)

96

97

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)

168 def 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)

175

176

177 def 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)

185

186

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)

195 def 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)

201

202

203@pytest.mark.parametrize(

204 ("angles", "expected"),

205 ntd.ENC_SIZE_NP_TESTS_SKIPNA,

206)

207 def 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)

212

213

214@pytest.mark.parametrize(

215 ("angles", "expected"),

216 ntd.ENC_SIZE_NP_TESTS_SKIPNA,

217)

218 def 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)

227

228

229@pytest.mark.parametrize(

230 ("angles", "expected"),

231 ntd.ENC_SIZE_NP_TESTS_SKIPNA,

232)

233 def 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)

245

246

247@pytest.mark.parametrize(

248 ("angles", "expected"),

249 ntd.ENC_SIZE_NP_TESTS_NOSKIPNA,

250)

251 def 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)

256

257

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)

267 def 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)

273

274

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)

283 def 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)

291

292

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)

362 def 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)

378

379

380@pytest.mark.parametrize(

381 "dims_kwargs",

382 [{"preserve_dims": ["letter", "banana"]}, {"reduce_dims": ["letter", "banana"]}],

383 ids=["preserve_dims", "reduce_dims"],

384)

385 def 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)

398

399

400 def 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)

410

411

412 def 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)