Coverage for nilearn/reporting/tests/test_glm_reporter.py: 0%
148 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
1import numpy as np
2import pandas as pd
3import pytest
5from nilearn._utils.data_gen import (
6 basic_paradigm,
7 generate_fake_fmri_data_and_design,
8 write_fake_bold_img,
9)
10from nilearn.conftest import _img_mask_mni, _make_surface_mask
11from nilearn.datasets import load_fsaverage
12from nilearn.glm.first_level import (
13 FirstLevelModel,
14)
15from nilearn.glm.second_level import SecondLevelModel
16from nilearn.maskers import NiftiMasker
17from nilearn.reporting import HTMLReport
18from nilearn.surface import SurfaceImage
21@pytest.fixture
22def rk():
23 return 3
26@pytest.fixture
27def contrasts(rk):
28 c = np.zeros((1, rk))
29 c[0][0] = 1
30 return c
33@pytest.fixture()
34def flm(rk):
35 """Generate first level model."""
36 shapes = ((7, 7, 7, 5),)
37 mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
38 shapes, rk=rk
39 )
40 # generate_fake_fmri_data_and_design
41 return FirstLevelModel().fit(fmri_data, design_matrices=design_matrices)
44@pytest.fixture()
45def slm():
46 """Generate a fitted second level model."""
47 shapes = ((7, 7, 7, 1),)
48 _, fmri_data, _ = generate_fake_fmri_data_and_design(shapes)
49 model = SecondLevelModel()
50 Y = [fmri_data[0]] * 2
51 X = pd.DataFrame([[1]] * 2, columns=["intercept"])
52 return model.fit(Y, design_matrix=X)
55def test_flm_report_no_activation_found(flm, contrasts):
56 """Check presence message of no activation found.
58 We use random data, so we should not get activations.
59 """
60 report = flm.generate_report(contrasts=contrasts)
61 assert "No suprathreshold cluster" in report.__str__()
64@pytest.mark.parametrize("model", [FirstLevelModel, SecondLevelModel])
65@pytest.mark.parametrize("bg_img", [_img_mask_mni(), _make_surface_mask()])
66def test_empty_surface_reports(tmp_path, model, bg_img):
67 """Test that empty reports on unfitted model can be generated."""
68 report = model(smoothing_fwhm=None).generate_report(bg_img=bg_img)
70 assert isinstance(report, HTMLReport)
72 report.save_as_html(tmp_path / "tmp.html")
73 assert (tmp_path / "tmp.html").exists()
76def test_flm_reporting_no_contrasts(flm):
77 """Test for model report can be generated with no contrasts."""
78 report = flm.generate_report(
79 plot_type="glass",
80 contrasts=None,
81 min_distance=15,
82 alpha=0.01,
83 threshold=2,
84 )
85 assert "No statistical map was provided." in report.__str__()
88def test_mask_coverage_in_report(flm):
89 """Check that how much image is included in mask is in the report."""
90 report = flm.generate_report()
91 assert "The mask includes" in report.__str__()
94@pytest.mark.parametrize("height_control", ["fdr", "bonferroni", None])
95def test_flm_reporting_height_control(flm, height_control, contrasts):
96 """Test for first level model reporting."""
97 report_flm = flm.generate_report(
98 contrasts=contrasts,
99 plot_type="glass",
100 height_control=height_control,
101 min_distance=15,
102 alpha=0.01,
103 threshold=2,
104 )
105 # catches & raises UnicodeEncodeError in HTMLDocument.get_iframe()
106 # in case certain unicode characters are mishandled,
107 # like the greek alpha symbol.
108 report_flm.get_iframe()
110 # glover is the default hrf so it should appear in report
111 assert "glover" in report_flm.__str__()
113 # cosine is the default drift model so it should appear in report
114 assert "cosine" in report_flm.__str__()
117@pytest.mark.timeout(0)
118@pytest.mark.parametrize("height_control", ["fpr", "fdr", "bonferroni", None])
119def test_slm_reporting_method(slm, height_control):
120 """Test for the second level reporting."""
121 c1 = np.eye(len(slm.design_matrix_.columns))[0]
122 report_slm = slm.generate_report(
123 c1, height_control=height_control, threshold=2, alpha=0.01
124 )
125 # catches & raises UnicodeEncodeError in HTMLDocument.get_iframe()
126 report_slm.get_iframe()
129@pytest.mark.timeout(0)
130def test_slm_with_flm_as_inputs(flm, contrasts):
131 """Test second level reporting when inputs are first level models."""
132 model = SecondLevelModel()
134 Y = [flm] * 3
135 X = pd.DataFrame([[1]] * 3, columns=["intercept"])
136 first_level_contrast = contrasts
138 model.fit(Y, design_matrix=X)
140 c1 = np.eye(len(model.design_matrix_.columns))[0]
142 model.generate_report(c1, first_level_contrast=first_level_contrast)
145def test_slm_with_dataframes_as_input(tmp_path, shape_3d_default):
146 """Test second level reporting when input is a dataframe."""
147 file_path = write_fake_bold_img(
148 file_path=tmp_path / "img.nii.gz", shape=shape_3d_default
149 )
151 dfcols = ["subject_label", "map_name", "effects_map_path"]
152 dfrows = [
153 ["01", "a", file_path],
154 ["02", "a", file_path],
155 ["03", "a", file_path],
156 ]
157 niidf = pd.DataFrame(dfrows, columns=dfcols)
159 model = SecondLevelModel().fit(niidf)
161 c1 = np.eye(len(model.design_matrix_.columns))[0]
163 model.generate_report(c1, first_level_contrast="a")
166@pytest.mark.parametrize("plot_type", ["slice", "glass"])
167def test_report_plot_type(flm, plot_type, contrasts):
168 """Smoke test for valid plot type."""
169 flm.generate_report(
170 contrasts=contrasts,
171 plot_type=plot_type,
172 threshold=2.76,
173 )
176@pytest.mark.parametrize("plot_type", ["slice", "glass"])
177@pytest.mark.parametrize("cut_coords", [None, (5, 4, 3)])
178def test_report_cut_coords(flm, plot_type, cut_coords, contrasts):
179 """Smoke test for valid cut_coords."""
180 flm.generate_report(
181 contrasts=contrasts,
182 cut_coords=cut_coords,
183 display_mode="z",
184 plot_type=plot_type,
185 threshold=2.76,
186 )
189def test_report_invalid_plot_type(matplotlib_pyplot, flm, contrasts): # noqa: ARG001
190 with pytest.raises(KeyError, match="junk"):
191 flm.generate_report(
192 contrasts=contrasts,
193 plot_type="junk",
194 threshold=2.76,
195 )
197 expected_error = (
198 "Invalid plot type provided. "
199 "Acceptable options are 'slice' or 'glass'."
200 )
202 with pytest.raises(ValueError, match=expected_error):
203 flm.generate_report(
204 contrasts=contrasts,
205 display_mode="glass",
206 plot_type="junk",
207 threshold=2.76,
208 )
211def test_masking_first_level_model(contrasts):
212 """Check that using NiftiMasker when instantiating FirstLevelModel \
213 doesn't raise Error when calling generate_report().
214 """
215 shapes, rk = ((7, 7, 7, 5),), 3
216 mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
217 shapes,
218 rk,
219 )
220 masker = NiftiMasker(mask_img=mask)
221 masker.fit(fmri_data)
222 flm = FirstLevelModel(mask_img=masker).fit(
223 fmri_data, design_matrices=design_matrices
224 )
226 report_flm = flm.generate_report(
227 contrasts=contrasts,
228 plot_type="glass",
229 height_control=None,
230 min_distance=15,
231 alpha=0.01,
232 threshold=2,
233 )
235 report_flm.get_iframe()
238def test_fir_delays_in_params(contrasts):
239 """Check that fir_delays is in the report when hrf_model is fir.
241 Also check that it's not in the report when using the default 'glover'.
242 """
243 shapes, rk = ((7, 7, 7, 5),), 3
244 _, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
245 shapes, rk
246 )
247 model = FirstLevelModel(hrf_model="fir", fir_delays=[1, 2, 3])
248 model.fit(fmri_data, design_matrices=design_matrices)
250 report = model.generate_report(contrasts=contrasts, threshold=0.1)
252 assert "fir_delays" in report.__str__()
255def test_drift_order_in_params(contrasts):
256 """Check that drift_order is in the report when parameter is drift_model is
257 polynomial.
258 """
259 shapes, rk = ((7, 7, 7, 5),), 3
260 _, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
261 shapes, rk
262 )
263 model = FirstLevelModel(drift_model="polynomial", drift_order=3)
264 model.fit(fmri_data, design_matrices=design_matrices)
266 report = model.generate_report(contrasts=contrasts)
268 assert "drift_order" in report.__str__()
271def test_flm_generate_report_surface_data(rng):
272 """Generate report from flm fitted surface.
274 Need a larger image to avoid issues with colormap.
275 """
276 t_r = 2.0
277 events = basic_paradigm()
278 n_scans = 10
280 mesh = load_fsaverage(mesh="fsaverage5")["pial"]
281 data = {}
282 for key, val in mesh.parts.items():
283 data_shape = (val.n_vertices, n_scans)
284 data_part = rng.normal(size=data_shape)
285 data[key] = data_part
286 fmri_data = SurfaceImage(mesh, data)
288 # using smoothing_fwhm for coverage
289 model = FirstLevelModel(t_r=t_r, smoothing_fwhm=None)
291 model.fit(fmri_data, events=events)
293 report = model.generate_report("c0", height_control=None)
295 assert isinstance(report, HTMLReport)
297 assert "Results table not available for surface data." in report.__str__()
300def test_flm_generate_report_surface_data_error(
301 surf_mask_1d, surf_img_2d, img_3d_mni
302):
303 """Generate report from flm fitted surface."""
304 model = FirstLevelModel(
305 mask_img=surf_mask_1d, t_r=2.0, smoothing_fwhm=None
306 )
307 events = basic_paradigm()
308 model.fit(surf_img_2d(9), events=events)
310 with pytest.raises(
311 TypeError, match="'bg_img' must a SurfaceImage instance"
312 ):
313 model.generate_report("c0", bg_img=img_3d_mni, height_control=None)
316@pytest.mark.timeout(0)
317def test_carousel_two_runs(
318 matplotlib_pyplot, # noqa: ARG001
319 flm,
320 slm,
321 contrasts,
322):
323 """Check that a carousel is present when there is more than 1 run."""
324 # Second level have a single "run" and do not need a carousel
325 report_slm = slm.generate_report()
327 assert 'id="carousel-navbar"' not in report_slm.__str__()
329 # first level model with one run : no run carousel
330 report_one_run = flm.generate_report(contrasts=contrasts)
332 assert 'id="carousel-navbar"' not in report_one_run.__str__()
334 # first level model with 2 runs : run carousel
335 rk = 6
336 shapes = ((7, 7, 7, 5), (7, 7, 7, 10))
337 _, fmri_data, design_matrices = generate_fake_fmri_data_and_design(
338 shapes, rk=rk
339 )
341 contrasts = np.zeros((1, rk))
342 contrasts[0][1] = 1
344 flm_two_runs = FirstLevelModel().fit(
345 fmri_data, design_matrices=design_matrices
346 )
348 report = flm_two_runs.generate_report(contrasts=contrasts)
350 assert 'id="carousel-navbar"' in report.__str__()