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

1""" 

2This module contains tests for scores.continuous.flip_flop 

3""" 

4import dask 

5import numpy as np 

6import pytest 

7import xarray as xr 

8 

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 

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) 

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) 

60 

61 

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) 

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) 

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) 

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) 

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) 

175 

176 

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) 

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) 

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) 

201 

202 

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) 

212 

213 

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) 

227 

228 

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) 

245 

246 

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) 

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) 

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) 

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) 

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) 

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) 

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) 

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) 

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) 

398 

399 

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) 

410 

411 

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)