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
« 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
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
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)
29ESTIMATORS_TO_CHECK = [SurfaceLabelsMasker(_sklearn_surf_label_img())]
31if SKLEARN_LT_1_6:
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)
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)
50else:
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)
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)
70def test_surface_label_masker_fit(surf_label_img):
71 """Test fit and check estimated attributes.
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()
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}
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 )
93 with pytest.warns(UserWarning, match="Dropping excess names values."):
94 masker = masker.fit()
96 assert masker.n_elements_ == 1
97 assert masker.labels_ == [0, 1]
98 assert masker.lut_["name"].to_list() == ["background", "bar"]
100 masker = SurfaceLabelsMasker(
101 labels_img=surf_label_img, labels=["background"]
102 )
104 with pytest.warns(UserWarning, match="Padding 'names' with 'unknown'"):
105 masker = masker.fit()
107 assert masker.n_elements_ == 1
108 assert masker.labels_ == [0, 1]
109 assert masker.lut_["name"].to_list() == ["background", "unknown"]
112def test_surface_label_masker_fit_with_lut(surf_label_img, tmp_path):
113 """Check passing lut is reflected in attributes.
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"]})
122 lut_tsv = tmp_path / "lut.tsv"
123 lut_df.to_csv(lut_tsv, sep="\t", index=False)
125 lut_csv = tmp_path / "lut.csv"
126 lut_df.to_csv(lut_csv, sep="\t", index=False)
128 for lut in [lut_tsv, lut_csv, lut_df, str(lut_tsv)]:
129 masker = SurfaceLabelsMasker(labels_img=surf_label_img, lut=lut).fit()
131 assert masker.n_elements_ == 1
132 assert masker.labels_ == [0, 1]
133 assert masker.lut_["name"].to_list() == ["background", "bar"]
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()
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
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.
171 Also a smoke test for different strategies.
172 """
173 masker = SurfaceLabelsMasker(labels_img=surf_label_img, strategy=strategy)
174 masker = masker.fit()
176 signal = masker.transform(surf_img_1d)
178 assert isinstance(signal, np.ndarray)
179 assert signal.shape == ()
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)
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)
203 with pytest.warns(
204 UserWarning,
205 match="the following labels were removed",
206 ):
207 masker = masker.fit()
209 n_timepoints = 5
210 signal = masker.transform(surf_img_2d(n_timepoints))
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_)
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 }
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 }
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 )
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 )
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 )
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 )
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 )
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.
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()
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)
333 assert_array_equal(signal, np.asarray(expected_signal))
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 }
345 assert_array_equal(img.data.parts["left"], expected_inverse_data["left"])
346 assert_array_equal(img.data.parts["right"], expected_inverse_data["right"])
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.
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()
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 }
386 surf_img_2d = SurfaceImage(surf_mesh, data)
387 signal = masker.transform(surf_img_2d)
389 assert signal.shape == (surf_img_2d.shape[1], masker.n_elements_)
391 expected_signal = np.asarray([expected_signal - 1, expected_signal + 1])
392 assert_array_equal(signal, expected_signal)
394 # also check the output of inverse_transform
395 img = masker.inverse_transform(signal)
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"])
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)
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)
465 with pytest.warns(
466 UserWarning,
467 match="the following labels were removed",
468 ):
469 masker = masker.fit()
471 n_timepoints = 5
472 signal = masker.transform(surf_img_2d(n_timepoints))
474 img_inverted = masker.inverse_transform(signal)
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)
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()
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()