Coverage for nilearn/maskers/tests/test_surface_labels_masker.py: 0%

161 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1import numpy as np 

2import pandas as pd 

3import pytest 

4from numpy.testing import assert_array_equal 

5from sklearn.utils.estimator_checks import parametrize_with_checks 

6 

7from nilearn._utils.estimator_checks import ( 

8 check_estimator, 

9 nilearn_check_estimator, 

10 return_expected_failed_checks, 

11) 

12from nilearn._utils.tags import SKLEARN_LT_1_6 

13from nilearn.conftest import _make_mesh 

14from nilearn.maskers import SurfaceLabelsMasker 

15from nilearn.surface import SurfaceImage 

16 

17 

18def _sklearn_surf_label_img(): 

19 """Create a sample surface label image using the sample mesh, 

20 just to use for scikit-learn checks. 

21 """ 

22 labels = { 

23 "left": np.asarray([1, 1, 2, 2]), 

24 "right": np.asarray([1, 1, 2, 2, 2]), 

25 } 

26 return SurfaceImage(_make_mesh(), labels) 

27 

28 

29ESTIMATORS_TO_CHECK = [SurfaceLabelsMasker(_sklearn_surf_label_img())] 

30 

31if SKLEARN_LT_1_6: 

32 

33 @pytest.mark.parametrize( 

34 "estimator, check, name", 

35 check_estimator(estimators=ESTIMATORS_TO_CHECK), 

36 ) 

37 def test_check_estimator_sklearn_valid(estimator, check, name): # noqa: ARG001 

38 """Check compliance with sklearn estimators.""" 

39 check(estimator) 

40 

41 @pytest.mark.xfail(reason="invalid checks should fail") 

42 @pytest.mark.parametrize( 

43 "estimator, check, name", 

44 check_estimator(estimators=ESTIMATORS_TO_CHECK, valid=False), 

45 ) 

46 def test_check_estimator_sklearn_invalid(estimator, check, name): # noqa: ARG001 

47 """Check compliance with sklearn estimators.""" 

48 check(estimator) 

49 

50else: 

51 

52 @parametrize_with_checks( 

53 estimators=ESTIMATORS_TO_CHECK, 

54 expected_failed_checks=return_expected_failed_checks, 

55 ) 

56 def test_check_estimator_sklearn(estimator, check): 

57 """Check compliance with sklearn estimators.""" 

58 check(estimator) 

59 

60 

61@pytest.mark.parametrize( 

62 "estimator, check, name", 

63 nilearn_check_estimator(estimators=ESTIMATORS_TO_CHECK), 

64) 

65def test_check_estimator_nilearn(estimator, check, name): # noqa: ARG001 

66 """Check compliance with sklearn estimators.""" 

67 check(estimator) 

68 

69 

70def test_surface_label_masker_fit(surf_label_img): 

71 """Test fit and check estimated attributes. 

72 

73 0 value in data is considered as background 

74 and should not be listed in the labels. 

75 """ 

76 masker = SurfaceLabelsMasker(labels_img=surf_label_img) 

77 masker = masker.fit() 

78 

79 assert masker.n_elements_ == 1 

80 assert masker.labels_ == [0, 1] 

81 assert masker._reporting_data is not None 

82 assert masker.lut_["name"].to_list() == ["0", "1"] 

83 assert masker.region_names_ == {1: "1"} 

84 assert masker.region_ids_ == {0: 0, 1: 1} 

85 

86 

87def test_surface_label_masker_fit_with_names(surf_label_img): 

88 """Check passing labels is reflected in attributes.""" 

89 masker = SurfaceLabelsMasker( 

90 labels_img=surf_label_img, labels=["background", "bar", "foo"] 

91 ) 

92 

93 with pytest.warns(UserWarning, match="Dropping excess names values."): 

94 masker = masker.fit() 

95 

96 assert masker.n_elements_ == 1 

97 assert masker.labels_ == [0, 1] 

98 assert masker.lut_["name"].to_list() == ["background", "bar"] 

99 

100 masker = SurfaceLabelsMasker( 

101 labels_img=surf_label_img, labels=["background"] 

102 ) 

103 

104 with pytest.warns(UserWarning, match="Padding 'names' with 'unknown'"): 

105 masker = masker.fit() 

106 

107 assert masker.n_elements_ == 1 

108 assert masker.labels_ == [0, 1] 

109 assert masker.lut_["name"].to_list() == ["background", "unknown"] 

110 

111 

112def test_surface_label_masker_fit_with_lut(surf_label_img, tmp_path): 

113 """Check passing lut is reflected in attributes. 

114 

115 Check that lut can be read from: 

116 - a tsv file (str or path) 

117 - a csv file (doc strings only mention TSV but testing for robustness) 

118 - a dataframe 

119 """ 

120 lut_df = pd.DataFrame({"index": [0, 1], "name": ["background", "bar"]}) 

121 

122 lut_tsv = tmp_path / "lut.tsv" 

123 lut_df.to_csv(lut_tsv, sep="\t", index=False) 

124 

125 lut_csv = tmp_path / "lut.csv" 

126 lut_df.to_csv(lut_csv, sep="\t", index=False) 

127 

128 for lut in [lut_tsv, lut_csv, lut_df, str(lut_tsv)]: 

129 masker = SurfaceLabelsMasker(labels_img=surf_label_img, lut=lut).fit() 

130 

131 assert masker.n_elements_ == 1 

132 assert masker.labels_ == [0, 1] 

133 assert masker.lut_["name"].to_list() == ["background", "bar"] 

134 

135 

136def test_surface_label_masker_error_names_and_lut(surf_label_img): 

137 """Cannot pass both look up table AND names.""" 

138 lut = pd.DataFrame({"index": [0, 1], "name": ["background", "bar"]}) 

139 masker = SurfaceLabelsMasker( 

140 labels_img=surf_label_img, labels=["background", "bar"], lut=lut 

141 ) 

142 with pytest.raises( 

143 ValueError, 

144 match="Pass either labels or a lookup table .* but not both.", 

145 ): 

146 masker.fit() 

147 

148 

149def test_surface_label_masker_fit_no_report(surf_label_img): 

150 """Check no report data is stored.""" 

151 masker = SurfaceLabelsMasker(labels_img=surf_label_img, reports=False) 

152 masker = masker.fit() 

153 assert masker._reporting_data is None 

154 

155 

156@pytest.mark.parametrize( 

157 "strategy", 

158 ( 

159 "variance", 

160 "minimum", 

161 "mean", 

162 "standard_deviation", 

163 "sum", 

164 "median", 

165 "maximum", 

166 ), 

167) 

168def test_surface_label_masker_transform(surf_label_img, surf_img_1d, strategy): 

169 """Test transform extract signals. 

170 

171 Also a smoke test for different strategies. 

172 """ 

173 masker = SurfaceLabelsMasker(labels_img=surf_label_img, strategy=strategy) 

174 masker = masker.fit() 

175 

176 signal = masker.transform(surf_img_1d) 

177 

178 assert isinstance(signal, np.ndarray) 

179 assert signal.shape == () 

180 

181 

182def test_surface_label_masker_transform_with_mask(surf_mesh, surf_img_2d): 

183 """Test transform extract signals with a mask and check warning.""" 

184 # create a labels image 

185 labels_data = { 

186 "left": np.asarray([1, 1, 1, 2]), 

187 "right": np.asarray([3, 3, 2, 2, 2]), 

188 } 

189 surf_label_img = SurfaceImage(surf_mesh, labels_data) 

190 

191 # create a mask image 

192 # we are keeping labels 1 and 2 out of 3 

193 # so we should only get signals for labels 1 and 2 

194 # plus masker should throw a warning that label 3 is being removed due to 

195 # mask 

196 mask_data = { 

197 "left": np.asarray([1, 1, 1, 1]), 

198 "right": np.asarray([0, 0, 1, 1, 1]), 

199 } 

200 surf_mask = SurfaceImage(surf_mesh, mask_data) 

201 masker = SurfaceLabelsMasker(labels_img=surf_label_img, mask_img=surf_mask) 

202 

203 with pytest.warns( 

204 UserWarning, 

205 match="the following labels were removed", 

206 ): 

207 masker = masker.fit() 

208 

209 n_timepoints = 5 

210 signal = masker.transform(surf_img_2d(n_timepoints)) 

211 

212 assert isinstance(signal, np.ndarray) 

213 expected_n_regions = 2 

214 assert masker.n_elements_ == expected_n_regions 

215 assert signal.shape == (n_timepoints, masker.n_elements_) 

216 

217 

218@pytest.fixture 

219def polydata_labels(): 

220 """Return polydata with 4 regions.""" 

221 return { 

222 "left": np.asarray([2, 0, 10, 1]), 

223 "right": np.asarray([10, 1, 20, 20, 0]), 

224 } 

225 

226 

227@pytest.fixture 

228def expected_mean_value(): 

229 """Return expected values for some specific labels.""" 

230 return { 

231 "1": 5, 

232 "2": 6, 

233 "10": 50, 

234 "20": 60, 

235 } 

236 

237 

238@pytest.fixture 

239def data_left_1d_with_expected_mean(rng, expected_mean_value): 

240 """Generate left data with given expected value for one sample.""" 

241 return np.asarray( 

242 [ 

243 expected_mean_value["2"], 

244 rng.random(), 

245 expected_mean_value["10"], 

246 expected_mean_value["1"], 

247 ] 

248 ) 

249 

250 

251@pytest.fixture 

252def data_right_1d_with_expected_mean(rng, expected_mean_value): 

253 """Generate right data with given expected value for one sample.""" 

254 return np.asarray( 

255 [ 

256 expected_mean_value["10"], 

257 expected_mean_value["1"], 

258 expected_mean_value["20"], 

259 expected_mean_value["20"], 

260 rng.random(), 

261 ] 

262 ) 

263 

264 

265@pytest.fixture 

266def expected_signal(expected_mean_value): 

267 """Return signal extract from data with expected mean.""" 

268 return np.asarray( 

269 [ 

270 expected_mean_value["1"], 

271 expected_mean_value["2"], 

272 expected_mean_value["10"], 

273 expected_mean_value["20"], 

274 ] 

275 ) 

276 

277 

278@pytest.fixture 

279def inverse_data_left_1d_with_expected_mean(expected_mean_value): 

280 """Return inversed left data with given expected value for one sample.""" 

281 return np.asarray( 

282 [ 

283 expected_mean_value["2"], 

284 0.0, 

285 expected_mean_value["10"], 

286 expected_mean_value["1"], 

287 ] 

288 ) 

289 

290 

291@pytest.fixture 

292def inverse_data_right_1d_with_expected_mean(expected_mean_value): 

293 """Return inversed right data with given expected value for one sample.""" 

294 return np.asarray( 

295 [ 

296 expected_mean_value["10"], 

297 expected_mean_value["1"], 

298 expected_mean_value["20"], 

299 expected_mean_value["20"], 

300 0.0, 

301 ] 

302 ) 

303 

304 

305def test_surface_label_masker_check_output_1d( 

306 surf_mesh, 

307 polydata_labels, 

308 expected_signal, 

309 data_left_1d_with_expected_mean, 

310 data_right_1d_with_expected_mean, 

311 inverse_data_left_1d_with_expected_mean, 

312 inverse_data_right_1d_with_expected_mean, 

313): 

314 """Check actual content of the transform and inverse_transform. 

315 

316 - Use a label mask with more than one label. 

317 - Use data with known content and expected mean. 

318 and background label data has random value. 

319 - Check that output data is properly averaged, 

320 even when labels are spread across hemispheres. 

321 """ 

322 surf_label_img = SurfaceImage(surf_mesh, polydata_labels) 

323 masker = SurfaceLabelsMasker(labels_img=surf_label_img) 

324 masker = masker.fit() 

325 

326 data = { 

327 "left": data_left_1d_with_expected_mean, 

328 "right": data_right_1d_with_expected_mean, 

329 } 

330 surf_img_1d = SurfaceImage(surf_mesh, data) 

331 signal = masker.transform(surf_img_1d) 

332 

333 assert_array_equal(signal, np.asarray(expected_signal)) 

334 

335 # also check the output of inverse_transform 

336 img = masker.inverse_transform(signal) 

337 assert img.shape[0] == surf_img_1d.shape[0] 

338 # expected inverse data is the same as the input data 

339 # but with the random value replaced by zeros 

340 expected_inverse_data = { 

341 "left": np.asarray(inverse_data_left_1d_with_expected_mean).T, 

342 "right": np.asarray(inverse_data_right_1d_with_expected_mean).T, 

343 } 

344 

345 assert_array_equal(img.data.parts["left"], expected_inverse_data["left"]) 

346 assert_array_equal(img.data.parts["right"], expected_inverse_data["right"]) 

347 

348 

349def test_surface_label_masker_check_output_2d( 

350 surf_mesh, 

351 polydata_labels, 

352 expected_mean_value, 

353 expected_signal, 

354 data_left_1d_with_expected_mean, 

355 data_right_1d_with_expected_mean, 

356): 

357 """Check actual content of the transform and inverse_transform when 

358 we have multiple timepoints. 

359 

360 - Use a label mask with more than one label. 

361 - Use data with known content and expected mean. 

362 and background label data has random value. 

363 - Check that output data is properly averaged, 

364 even when labels are spread across hemispheres. 

365 """ 

366 surf_label_img = SurfaceImage(surf_mesh, polydata_labels) 

367 masker = SurfaceLabelsMasker(labels_img=surf_label_img) 

368 masker = masker.fit() 

369 

370 # Now with 2 'time points' 

371 data = { 

372 "left": np.asarray( 

373 [ 

374 data_left_1d_with_expected_mean - 1, 

375 data_left_1d_with_expected_mean + 1, 

376 ] 

377 ).T, 

378 "right": np.asarray( 

379 [ 

380 data_right_1d_with_expected_mean - 1, 

381 data_right_1d_with_expected_mean + 1, 

382 ] 

383 ).T, 

384 } 

385 

386 surf_img_2d = SurfaceImage(surf_mesh, data) 

387 signal = masker.transform(surf_img_2d) 

388 

389 assert signal.shape == (surf_img_2d.shape[1], masker.n_elements_) 

390 

391 expected_signal = np.asarray([expected_signal - 1, expected_signal + 1]) 

392 assert_array_equal(signal, expected_signal) 

393 

394 # also check the output of inverse_transform 

395 img = masker.inverse_transform(signal) 

396 

397 assert img.shape[0] == surf_img_2d.shape[0] 

398 # expected inverse data is the same as the input data 

399 # but with the random values replaced by zeros 

400 expected_inverse_data = { 

401 "left": np.asarray( 

402 [ 

403 [ 

404 expected_mean_value["2"] - 1, 

405 0.0, 

406 expected_mean_value["10"] - 1, 

407 expected_mean_value["1"] - 1, 

408 ], 

409 [ 

410 expected_mean_value["2"] + 1, 

411 0.0, 

412 expected_mean_value["10"] + 1, 

413 expected_mean_value["1"] + 1, 

414 ], 

415 ] 

416 ).T, 

417 "right": np.asarray( 

418 [ 

419 [ 

420 expected_mean_value["10"] - 1, 

421 expected_mean_value["1"] - 1, 

422 expected_mean_value["20"] - 1, 

423 expected_mean_value["20"] - 1, 

424 0.0, 

425 ], 

426 [ 

427 expected_mean_value["10"] + 1, 

428 expected_mean_value["1"] + 1, 

429 expected_mean_value["20"] + 1, 

430 expected_mean_value["20"] + 1, 

431 0.0, 

432 ], 

433 ] 

434 ).T, 

435 } 

436 assert_array_equal(img.data.parts["left"], expected_inverse_data["left"]) 

437 assert_array_equal(img.data.parts["right"], expected_inverse_data["right"]) 

438 

439 

440def test_surface_label_masker_inverse_transform_with_mask( 

441 surf_mesh, surf_img_2d 

442): 

443 """Test inverse_transform with mask: inverted image's shape, warning if 

444 mask removes labels and data corresponding to removed labels is zeros. 

445 """ 

446 # create a labels image 

447 labels_data = { 

448 "left": np.asarray([1, 1, 1, 2]), 

449 "right": np.asarray([3, 3, 2, 2, 2]), 

450 } 

451 surf_label_img = SurfaceImage(surf_mesh, labels_data) 

452 

453 # create a mask image 

454 # we are keeping labels 1 and 3 out of 3 

455 # so we should only get signals for labels 1 and 3 

456 # plus masker should throw a warning that label 2 is being removed due to 

457 # mask 

458 mask_data = { 

459 "left": np.asarray([1, 1, 1, 0]), 

460 "right": np.asarray([1, 1, 0, 0, 0]), 

461 } 

462 surf_mask = SurfaceImage(surf_mesh, mask_data) 

463 masker = SurfaceLabelsMasker(labels_img=surf_label_img, mask_img=surf_mask) 

464 

465 with pytest.warns( 

466 UserWarning, 

467 match="the following labels were removed", 

468 ): 

469 masker = masker.fit() 

470 

471 n_timepoints = 5 

472 signal = masker.transform(surf_img_2d(n_timepoints)) 

473 

474 img_inverted = masker.inverse_transform(signal) 

475 

476 assert img_inverted.shape == surf_img_2d(n_timepoints).shape 

477 # the data for label 2 should be zeros 

478 assert np.all(img_inverted.data.parts["left"][-1, :] == 0) 

479 assert np.all(img_inverted.data.parts["right"][2:, :] == 0) 

480 

481 

482def test_surface_label_masker_labels_img_none(): 

483 """Test that an error is raised when labels_img is None.""" 

484 with pytest.raises( 

485 ValueError, 

486 match="provide a labels_img to the masker", 

487 ): 

488 SurfaceLabelsMasker(labels_img=None).fit() 

489 

490 

491def test_error_wrong_strategy(surf_label_img): 

492 """Throw error for unsupported strategies.""" 

493 masker = SurfaceLabelsMasker(labels_img=surf_label_img, strategy="foo") 

494 with pytest.raises(ValueError, match="Invalid strategy 'foo'."): 

495 masker.fit()