Coverage for nilearn/conftest.py: 45%
328 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Configuration and extra fixtures for pytest."""
3import warnings
5import nibabel
6import numpy as np
7import pandas as pd
8import pytest
9from nibabel import Nifti1Image
11from nilearn import image
12from nilearn._utils.data_gen import (
13 generate_fake_fmri,
14 generate_labeled_regions,
15 generate_maps,
16)
17from nilearn._utils.helpers import is_matplotlib_installed
19# we need to import these fixtures even if not used in this module
20from nilearn.datasets.tests._testing import (
21 request_mocker, # noqa: F401
22 temp_nilearn_data_dir, # noqa: F401
23)
24from nilearn.surface import (
25 InMemoryMesh,
26 PolyMesh,
27 SurfaceImage,
28)
30collect_ignore = ["datasets/data/convert_templates.py"]
31collect_ignore_glob = ["reporting/_visual_testing/*"]
33# Plotting tests are skipped if matplotlib is missing.
34# If the version is greater than the minimum one we support
35# We skip the tests where the generated figures are compared to a baseline.
37if is_matplotlib_installed(): 37 ↛ 38line 37 didn't jump to line 38 because the condition on line 37 was never true
38 import matplotlib
40 from nilearn._utils.helpers import (
41 OPTIONAL_MATPLOTLIB_MIN_VERSION,
42 compare_version,
43 )
45 if compare_version(
46 matplotlib.__version__, ">", OPTIONAL_MATPLOTLIB_MIN_VERSION
47 ):
48 collect_ignore.extend(
49 [
50 "plotting/tests/test_baseline_comparisons.py",
51 ]
52 )
54else:
55 collect_ignore.extend(
56 [
57 "_utils/plotting.py",
58 "plotting",
59 "reporting/html_report.py",
60 "reporting/tests/test_html_report.py",
61 ]
62 )
63 matplotlib = None # type: ignore[assignment]
66def pytest_configure(config): # noqa: ARG001
67 """Use Agg so that no figures pop up."""
68 if matplotlib is not None: 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true
69 matplotlib.use("Agg", force=True)
72@pytest.fixture(autouse=True)
73def no_int64_nifti(monkeypatch):
74 """Prevent creating or writing a Nift1Image containing 64-bit ints.
76 It is easy to create such images by mistake because Numpy uses int64 by
77 default, but tools like FSL fail to read them and Nibabel will refuse to
78 write them in the future.
80 For tests that do need to manipulate int64 images, it is always possible to
81 disable this fixture by parametrizing a test to override it:
83 @pytest.mark.parametrize("no_int64_nifti", [None])
84 def test_behavior_when_user_provides_int64_img():
85 # ...
87 But by default it is used automatically so that Nilearn doesn't create such
88 images by mistake.
90 """
91 forbidden_types = (np.int64, np.uint64)
92 error_msg = (
93 "Creating or saving an image containing 64-bit ints is forbidden."
94 )
96 to_filename = nibabel.nifti1.Nifti1Image.to_filename
98 def checked_to_filename(img, filename):
99 assert image.get_data(img).dtype not in forbidden_types, error_msg
100 return to_filename(img, filename)
102 monkeypatch.setattr(
103 "nibabel.nifti1.Nifti1Image.to_filename", checked_to_filename
104 )
106 init = nibabel.nifti1.Nifti1Image.__init__
108 def checked_init(self, dataobj, *args, **kwargs):
109 assert dataobj.dtype not in forbidden_types, error_msg
110 return init(self, dataobj, *args, **kwargs)
112 monkeypatch.setattr("nibabel.nifti1.Nifti1Image.__init__", checked_init)
115@pytest.fixture(autouse=True)
116def close_all():
117 """Close all matplotlib figures."""
118 yield
119 if matplotlib is not None: 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true
120 import matplotlib.pyplot as plt
122 plt.close("all") # takes < 1 us so just always do it
125@pytest.fixture(autouse=True)
126def suppress_specific_warning():
127 """Ignore internal deprecation warnings."""
128 with warnings.catch_warnings():
129 messages = (
130 "The `darkness` parameter will be deprecated.*|"
131 "In release 0.13, this fetcher will return a dictionary.*|"
132 "The default strategy for standardize.*|"
133 "The 'fetch_bids_langloc_dataset' function will be removed.*|"
134 )
135 warnings.filterwarnings(
136 "ignore",
137 message=messages,
138 category=DeprecationWarning,
139 )
140 yield
143# ------------------------ RNG ------------------------#
146def _rng(seed=42):
147 return np.random.default_rng(seed)
150@pytest.fixture()
151def rng():
152 """Return a seeded random number generator."""
153 return _rng()
156# ------------------------ AFFINES ------------------------#
159def _affine_mni():
160 """Return an affine corresponding to 2mm isotropic MNI template.
162 Mostly used for set up in other fixtures in other testing modules.
163 """
164 return np.array(
165 [
166 [2.0, 0.0, 0.0, -98.0],
167 [0.0, 2.0, 0.0, -134.0],
168 [0.0, 0.0, 2.0, -72.0],
169 [0.0, 0.0, 0.0, 1.0],
170 ]
171 )
174@pytest.fixture()
175def affine_mni():
176 """Return an affine corresponding to 2mm isotropic MNI template."""
177 return _affine_mni()
180def _affine_eye():
181 """Return an identity matrix affine.
183 Mostly used for set up in other fixtures in other testing modules.
184 """
185 return np.eye(4)
188@pytest.fixture()
189def affine_eye():
190 """Return an identity matrix affine."""
191 return _affine_eye()
194# ------------------------ SHAPES ------------------------#
197def _shape_3d_default():
198 """Return default shape for a 3D image.
200 Mostly used for set up in other fixtures in other testing modules.
201 """
202 # avoid having identical shapes values,
203 # because this fails to detect if the code does not handle dimensions well.
204 return (7, 8, 9)
207def _shape_3d_large():
208 """Shape usually used for maps images.
210 Mostly used for set up in other fixtures in other testing modules.
211 """
212 # avoid having identical shapes values,
213 # because this fails to detect if the code does not handle dimensions well.
214 return (29, 30, 31)
217def _shape_4d_default():
218 """Return default shape for a 4D image.
220 Mostly used for set up in other fixtures in other testing modules.
221 """
222 # avoid having identical shapes values,
223 # because this fails to detect if the code does not handle dimensions well.
224 return (7, 8, 9, 5)
227def _shape_4d_medium():
228 """Return default shape for a long 4D image."""
229 # avoid having identical shapes values,
230 # because this fails to detect if the code does not handle dimensions well.
231 return (7, 8, 9, 100)
234def _shape_4d_long():
235 """Return default shape for a long 4D image."""
236 # avoid having identical shapes values,
237 # because this fails to detect if the code does not handle dimensions well.
238 return (7, 8, 9, 1500)
241@pytest.fixture()
242def shape_3d_default():
243 """Return default shape for a 3D image."""
244 return _shape_3d_default()
247@pytest.fixture
248def shape_3d_large():
249 """Shape usually used for maps images."""
250 return _shape_3d_large()
253@pytest.fixture()
254def shape_4d_default():
255 """Return default shape for a 4D image."""
256 return _shape_4d_default()
259@pytest.fixture()
260def shape_4d_long():
261 """Return long shape for a 4D image."""
262 return _shape_4d_long()
265def _img_zeros(shape, affine):
266 return Nifti1Image(np.zeros(shape), affine)
269def _img_ones(shape, affine):
270 return Nifti1Image(np.ones(shape), affine)
273# ------------------------ 3D IMAGES ------------------------#
276def _img_3d_rand(affine=None):
277 """Return random 3D Nifti1Image in MNI space.
279 Mostly used for set up in other fixtures in other testing modules.
280 """
281 if affine is None:
282 affine = _affine_eye()
283 data = _rng().random(_shape_3d_default())
284 return Nifti1Image(data, affine)
287@pytest.fixture()
288def img_3d_rand_eye():
289 """Return random 3D Nifti1Image in MNI space."""
290 return _img_3d_rand()
293def _img_3d_mni(affine=None):
294 if affine is None:
295 affine = _affine_mni()
296 data_positive = np.zeros((7, 7, 3))
297 rng = _rng()
298 data_rng = rng.random((7, 7, 3))
299 data_positive[1:-1, 2:-1, 1:] = data_rng[1:-1, 2:-1, 1:]
300 return Nifti1Image(data_positive, affine)
303@pytest.fixture()
304def img_3d_mni():
305 """Return a default random 3D Nifti1Image in MNI space."""
306 return _img_3d_mni()
309@pytest.fixture()
310def img_3d_mni_as_file(tmp_path):
311 """Return path to a random 3D Nifti1Image in MNI space saved to disk."""
312 filename = tmp_path / "img.nii"
313 _img_3d_mni().to_filename(filename)
314 return filename
317def _img_3d_zeros(shape=None, affine=None):
318 """Return a default zeros filled 3D Nifti1Image (identity affine).
320 Mostly used for set up in other fixtures in other testing modules.
321 """
322 if shape is None:
323 shape = _shape_3d_default()
324 if affine is None:
325 affine = _affine_eye()
326 return _img_zeros(shape, affine)
329@pytest.fixture
330def img_3d_zeros_eye():
331 """Return a zeros-filled 3D Nifti1Image (identity affine)."""
332 return _img_3d_zeros()
335def _img_3d_ones(shape=None, affine=None):
336 """Return a ones-filled 3D Nifti1Image (identity affine).
338 Mostly used for set up in other fixtures in other testing modules.
339 """
340 if shape is None:
341 shape = _shape_3d_default()
342 if affine is None:
343 affine = _affine_eye()
344 return _img_ones(shape, affine)
347@pytest.fixture
348def img_3d_ones_eye():
349 """Return a ones-filled 3D Nifti1Image (identity affine)."""
350 return _img_3d_ones()
353@pytest.fixture
354def img_3d_ones_mni():
355 """Return a ones-filled 3D Nifti1Image (identity affine)."""
356 return _img_3d_ones(shape=_shape_3d_default(), affine=_affine_mni())
359def _mask_data():
360 mask_data = np.zeros(_shape_3d_default(), dtype="int32")
361 mask_data[3:6, 3:6, 3:6] = 1
362 return mask_data
365def _img_mask_mni():
366 """Return a 3D nifti mask in MNI space with some 1s in the center."""
367 return Nifti1Image(_mask_data(), _affine_mni())
370@pytest.fixture
371def img_mask_mni():
372 """Return a 3D nifti mask in MNI space with some 1s in the center."""
373 return _img_mask_mni()
376def _img_mask_eye():
377 """Return a 3D nifti mask with identity affine with 1s in the center."""
378 return Nifti1Image(_mask_data(), _affine_eye())
381@pytest.fixture
382def img_mask_eye():
383 """Return a 3D nifti mask with identity affine with 1s in the center."""
384 return _img_mask_eye()
387# ------------------------ 4D IMAGES ------------------------#
390def _img_4d_zeros(shape=None, affine=None):
391 """Return a default zeros filled 4D Nifti1Image (identity affine).
393 Mostly used for set up in other fixtures in other testing modules.
394 """
395 if shape is None:
396 shape = _shape_4d_default()
397 if affine is None:
398 affine = _affine_eye()
399 return _img_zeros(shape, affine)
402def _img_4d_rand_eye():
403 """Return a default random filled 4D Nifti1Image (identity affine)."""
404 data = _rng().random(_shape_4d_default())
405 return Nifti1Image(data, _affine_eye())
408def _img_4d_rand_eye_medium():
409 """Return a random 4D Nifti1Image (identity affine, many volumes)."""
410 data = _rng().random(_shape_4d_medium())
411 return Nifti1Image(data, _affine_eye())
414def _img_4d_mni(shape=None, affine=None):
415 if shape is None:
416 shape = _shape_4d_default()
417 if affine is None:
418 affine = _affine_mni()
419 return Nifti1Image(_rng().uniform(size=shape), affine=affine)
422@pytest.fixture
423def img_4d_zeros_eye():
424 """Return a default zeros filled 4D Nifti1Image (identity affine)."""
425 return _img_4d_zeros()
428@pytest.fixture
429def img_4d_ones_eye():
430 """Return a default ones filled 4D Nifti1Image (identity affine)."""
431 return _img_ones(_shape_4d_default(), _affine_eye())
434@pytest.fixture
435def img_4d_rand_eye():
436 """Return a default random filled 4D Nifti1Image (identity affine)."""
437 return _img_4d_rand_eye()
440@pytest.fixture
441def img_4d_mni():
442 """Return a default random filled 4D Nifti1Image."""
443 return _img_4d_mni()
446@pytest.fixture
447def img_4d_rand_eye_medium():
448 """Return a default random filled 4D Nifti1Image of medium length."""
449 return _img_4d_rand_eye_medium()
452@pytest.fixture
453def img_4d_long_mni(rng, shape_4d_long, affine_mni):
454 """Return a default random filled long 4D Nifti1Image."""
455 return Nifti1Image(rng.uniform(size=shape_4d_long), affine=affine_mni)
458# ------------------------ ATLAS, LABELS, MAPS ------------------------#
461@pytest.fixture()
462def img_atlas(shape_3d_default, affine_mni):
463 """Return an atlas and its labels."""
464 atlas = np.ones(shape_3d_default, dtype="int32")
465 atlas[2:5, :, :] = 2
466 atlas[5:8, :, :] = 3
467 return {
468 "img": Nifti1Image(atlas, affine_mni),
469 "labels": {
470 "gm": 1,
471 "wm": 2,
472 "csf": 3,
473 },
474 }
477def _n_regions():
478 """Return a default number of regions for maps."""
479 return 9
482@pytest.fixture
483def n_regions():
484 """Return a default number of regions for maps."""
485 return _n_regions()
488def _img_maps(n_regions=None):
489 """Generate a default map image."""
490 if n_regions is None:
491 n_regions = _n_regions()
492 return generate_maps(
493 shape=_shape_3d_default(), n_regions=n_regions, affine=_affine_eye()
494 )[0]
497@pytest.fixture
498def img_maps():
499 """Generate fixture for default map image."""
500 return _img_maps()
503def _img_labels():
504 """Generate fixture for default label image.
506 DO NOT CHANGE n_regions (some tests expect this value).
507 """
508 return generate_labeled_regions(
509 shape=_shape_3d_default(),
510 affine=_affine_eye(),
511 n_regions=_n_regions(),
512 )
515@pytest.fixture
516def img_labels():
517 """Generate fixture for default label image."""
518 return _img_labels()
521@pytest.fixture
522def length():
523 """Return a default length for 4D images."""
524 return 10
527@pytest.fixture
528def img_fmri(shape_3d_default, affine_eye, length):
529 """Return a default length for fmri images."""
530 return generate_fake_fmri(
531 shape_3d_default, affine=affine_eye, length=length
532 )[0]
535# ------------------------ SURFACE ------------------------#
536@pytest.fixture
537def single_mesh(rng):
538 """Create random coordinates and faces for a single mesh.
540 This does not generate meaningful surfaces.
541 """
542 coords = rng.random((20, 3))
543 faces = rng.integers(coords.shape[0], size=(30, 3))
544 return [coords, faces]
547@pytest.fixture
548def in_memory_mesh(single_mesh):
549 """Create a random InMemoryMesh.
551 This does not generate meaningful surfaces.
552 """
553 coords, faces = single_mesh
554 return InMemoryMesh(coordinates=coords, faces=faces)
557def _make_mesh():
558 """Create a sample mesh with two parts: left and right, and total of
559 9 vertices and 10 faces.
561 The left part is a tetrahedron with four vertices and four faces.
562 The right part is a pyramid with five vertices and six faces.
563 """
564 left_coords = np.asarray([[0.0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]])
565 left_faces = np.asarray([[1, 0, 2], [0, 1, 3], [0, 3, 2], [1, 2, 3]])
566 right_coords = (
567 np.asarray([[0.0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1]])
568 + 2.0
569 )
570 right_faces = np.asarray(
571 [
572 [0, 1, 4],
573 [0, 3, 1],
574 [1, 3, 2],
575 [1, 2, 4],
576 [2, 3, 4],
577 [0, 4, 3],
578 ]
579 )
580 return PolyMesh(
581 left=InMemoryMesh(left_coords, left_faces),
582 right=InMemoryMesh(right_coords, right_faces),
583 )
586@pytest.fixture()
587def surf_mesh():
588 """Return _make_mesh as a function allowing it to be used as a fixture."""
589 return _make_mesh()
592def _make_surface_img(n_samples=1):
593 mesh = _make_mesh()
594 data = {}
595 for i, (key, val) in enumerate(mesh.parts.items()):
596 data_shape = (val.n_vertices, n_samples)
597 data_part = (
598 np.arange(np.prod(data_shape)).reshape(data_shape[::-1])
599 ) * 10**i
600 data[key] = data_part.astype(float).T
601 return SurfaceImage(mesh, data)
604@pytest.fixture
605def surf_img_2d():
606 """Create a sample surface image using the sample mesh.
607 This will add some random data to the vertices of the mesh.
608 The shape of the data will be (n_vertices, n_samples).
609 n_samples by default is 1.
610 """
611 return _make_surface_img
614@pytest.fixture
615def surf_img_1d():
616 """Create a sample surface image using the sample mesh.
617 This will add some random data to the vertices of the mesh.
618 The shape of the data will be (n_vertices,).
619 """
620 img = _make_surface_img(n_samples=1)
621 img.data.parts["left"] = np.squeeze(img.data.parts["left"])
622 img.data.parts["right"] = np.squeeze(img.data.parts["right"])
623 return img
626def _make_surface_mask(n_zeros=4):
627 mesh = _make_mesh()
628 data = {}
629 for key, val in mesh.parts.items():
630 data_shape = (val.n_vertices, 1)
631 data_part = np.ones(data_shape, dtype=int)
632 for i in range(n_zeros // 2):
633 data_part[i, ...] = 0
634 data_part = data_part.astype(bool)
635 data[key] = data_part
636 return SurfaceImage(mesh, data)
639def _surf_mask_1d():
640 """Create a sample surface mask using the sample mesh.
641 This will create a mask with n_zeros zeros (default is 4) and the
642 rest ones.
644 The shape of the data will be (n_vertices,).
645 """
646 mask = _make_surface_mask()
647 mask.data.parts["left"] = np.squeeze(mask.data.parts["left"])
648 mask.data.parts["right"] = np.squeeze(mask.data.parts["right"])
650 return mask
653@pytest.fixture
654def surf_mask_1d():
655 """Create a sample surface mask using the sample mesh.
656 This will create a mask with n_zeros zeros (default is 4) and the
657 rest ones.
659 The shape of the data will be (n_vertices,).
660 """
661 return _surf_mask_1d()
664@pytest.fixture
665def surf_mask_2d():
666 """Create a sample surface mask using the sample mesh.
667 This will create a mask with n_zeros zeros (default is 4) and the
668 rest ones.
670 The shape of the data will be (n_vertices, 1). Could be useful for testing
671 input validation where we throw an error if the mask is not 1D.
672 """
673 return _make_surface_mask
676@pytest.fixture
677def surf_label_img(surf_mesh):
678 """Return a sample surface label image using the sample mesh.
679 Has two regions with values 0 and 1 respectively.
680 """
681 data = {
682 "left": np.asarray([0, 0, 1, 1]),
683 "right": np.asarray([1, 1, 0, 0, 0]),
684 }
685 return SurfaceImage(surf_mesh, data)
688@pytest.fixture
689def surf_three_labels_img(surf_mesh):
690 """Return a sample surface label image using the sample mesh.
691 Has 3 regions with values 0, 1 and 2.
692 """
693 data = {
694 "left": np.asarray([0, 0, 1, 1]),
695 "right": np.asarray([1, 1, 0, 2, 0]),
696 }
697 return SurfaceImage(surf_mesh, data)
700def _surf_maps_img():
701 """Return a sample surface map image using the sample mesh.
702 Has 6 regions in total: 3 in both, 1 only in left and 2 only in right.
703 Later we multiply the data with random "probability" values to make it
704 more realistic.
705 """
706 data = {
707 "left": np.asarray(
708 [
709 [1, 1, 0, 1, 0, 0],
710 [0, 1, 1, 1, 0, 0],
711 [1, 0, 1, 1, 0, 0],
712 [1, 1, 1, 0, 0, 0],
713 ]
714 ),
715 "right": np.asarray(
716 [
717 [1, 0, 0, 0, 1, 1],
718 [1, 1, 0, 0, 1, 1],
719 [0, 1, 1, 0, 1, 1],
720 [1, 1, 1, 0, 0, 1],
721 [0, 0, 1, 0, 0, 1],
722 ]
723 ),
724 }
725 # multiply with random "probability" values
726 data = {
727 part: data[part] * _rng().random(data[part].shape) for part in data
728 }
729 return SurfaceImage(_make_mesh(), data)
732@pytest.fixture
733def surf_maps_img():
734 """Return a sample surface map as fixture."""
735 return _surf_maps_img()
738def _flip_surf_img_parts(poly_obj):
739 """Flip hemispheres of a surface image data or mesh."""
740 keys = list(poly_obj.parts.keys())
741 keys = [keys[-1]] + keys[:-1]
742 return dict(zip(keys, poly_obj.parts.values()))
745@pytest.fixture
746def flip_surf_img_parts():
747 """Flip hemispheres of a surface image data or mesh."""
748 return _flip_surf_img_parts
751def _flip_surf_img(img):
752 """Flip hemispheres of a surface image."""
753 return SurfaceImage(
754 _flip_surf_img_parts(img.mesh), _flip_surf_img_parts(img.data)
755 )
758@pytest.fixture
759def flip_surf_img():
760 """Flip hemispheres of a surface image."""
761 return _flip_surf_img
764def _drop_surf_img_part(img, part_name="right"):
765 """Remove one hemisphere from a SurfaceImage."""
766 mesh_parts = img.mesh.parts.copy()
767 mesh_parts.pop(part_name)
768 data_parts = img.data.parts.copy()
769 data_parts.pop(part_name)
770 return SurfaceImage(mesh_parts, data_parts)
773@pytest.fixture
774def drop_surf_img_part():
775 """Remove one hemisphere from a SurfaceImage."""
776 return _drop_surf_img_part
779def _make_surface_img_and_design(n_samples=5):
780 des = pd.DataFrame(
781 _rng().standard_normal((n_samples, 3)), columns=["", "", ""]
782 )
783 return _make_surface_img(n_samples), des
786@pytest.fixture()
787def surface_glm_data():
788 """Create a surface image and design matrix for testing."""
789 return _make_surface_img_and_design
792# ------------------------ PLOTTING ------------------------#
795@pytest.fixture(scope="function")
796def matplotlib_pyplot():
797 """Set up and teardown fixture for matplotlib.
799 This fixture checks if we can import matplotlib. If not, the tests will be
800 skipped. Otherwise, we close the figures before and after running the
801 functions.
803 Returns
804 -------
805 pyplot : module
806 The ``matplotlib.pyplot`` module.
807 """
808 pyplot = pytest.importorskip("matplotlib.pyplot")
809 pyplot.close("all")
810 yield pyplot
811 pyplot.close("all")
814@pytest.fixture(scope="function")
815def plotly():
816 """Check if we can import plotly.
818 If not, the tests will be skipped.
820 Returns
821 -------
822 plotly : module
823 The ``plotly`` module.
824 """
825 yield pytest.importorskip(
826 "plotly", reason="Plotly is not installed; required to run the tests!"
827 )
830@pytest.fixture
831def transparency_image(rng, affine_mni):
832 """Return 3D image to use as transparency image.
834 Make sure that values are not just between 0 and 1.
835 """
836 data_positive = np.zeros((7, 7, 3))
837 data_rng = rng.random((7, 7, 3)) * 10 - 5
838 data_positive[1:-1, 2:-1, 1:] = data_rng[1:-1, 2:-1, 1:]
839 return Nifti1Image(data_positive, affine_mni)