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

1import numpy as np 

2import pandas as pd 

3import pytest 

4 

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 

19 

20 

21@pytest.fixture 

22def rk(): 

23 return 3 

24 

25 

26@pytest.fixture 

27def contrasts(rk): 

28 c = np.zeros((1, rk)) 

29 c[0][0] = 1 

30 return c 

31 

32 

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) 

42 

43 

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) 

53 

54 

55def test_flm_report_no_activation_found(flm, contrasts): 

56 """Check presence message of no activation found. 

57 

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__() 

62 

63 

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) 

69 

70 assert isinstance(report, HTMLReport) 

71 

72 report.save_as_html(tmp_path / "tmp.html") 

73 assert (tmp_path / "tmp.html").exists() 

74 

75 

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__() 

86 

87 

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__() 

92 

93 

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() 

109 

110 # glover is the default hrf so it should appear in report 

111 assert "glover" in report_flm.__str__() 

112 

113 # cosine is the default drift model so it should appear in report 

114 assert "cosine" in report_flm.__str__() 

115 

116 

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() 

127 

128 

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() 

133 

134 Y = [flm] * 3 

135 X = pd.DataFrame([[1]] * 3, columns=["intercept"]) 

136 first_level_contrast = contrasts 

137 

138 model.fit(Y, design_matrix=X) 

139 

140 c1 = np.eye(len(model.design_matrix_.columns))[0] 

141 

142 model.generate_report(c1, first_level_contrast=first_level_contrast) 

143 

144 

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 ) 

150 

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) 

158 

159 model = SecondLevelModel().fit(niidf) 

160 

161 c1 = np.eye(len(model.design_matrix_.columns))[0] 

162 

163 model.generate_report(c1, first_level_contrast="a") 

164 

165 

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 ) 

174 

175 

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 ) 

187 

188 

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 ) 

196 

197 expected_error = ( 

198 "Invalid plot type provided. " 

199 "Acceptable options are 'slice' or 'glass'." 

200 ) 

201 

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 ) 

209 

210 

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 ) 

225 

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 ) 

234 

235 report_flm.get_iframe() 

236 

237 

238def test_fir_delays_in_params(contrasts): 

239 """Check that fir_delays is in the report when hrf_model is fir. 

240 

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) 

249 

250 report = model.generate_report(contrasts=contrasts, threshold=0.1) 

251 

252 assert "fir_delays" in report.__str__() 

253 

254 

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) 

265 

266 report = model.generate_report(contrasts=contrasts) 

267 

268 assert "drift_order" in report.__str__() 

269 

270 

271def test_flm_generate_report_surface_data(rng): 

272 """Generate report from flm fitted surface. 

273 

274 Need a larger image to avoid issues with colormap. 

275 """ 

276 t_r = 2.0 

277 events = basic_paradigm() 

278 n_scans = 10 

279 

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) 

287 

288 # using smoothing_fwhm for coverage 

289 model = FirstLevelModel(t_r=t_r, smoothing_fwhm=None) 

290 

291 model.fit(fmri_data, events=events) 

292 

293 report = model.generate_report("c0", height_control=None) 

294 

295 assert isinstance(report, HTMLReport) 

296 

297 assert "Results table not available for surface data." in report.__str__() 

298 

299 

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) 

309 

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) 

314 

315 

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() 

326 

327 assert 'id="carousel-navbar"' not in report_slm.__str__() 

328 

329 # first level model with one run : no run carousel 

330 report_one_run = flm.generate_report(contrasts=contrasts) 

331 

332 assert 'id="carousel-navbar"' not in report_one_run.__str__() 

333 

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 ) 

340 

341 contrasts = np.zeros((1, rk)) 

342 contrasts[0][1] = 1 

343 

344 flm_two_runs = FirstLevelModel().fit( 

345 fmri_data, design_matrices=design_matrices 

346 ) 

347 

348 report = flm_two_runs.generate_report(contrasts=contrasts) 

349 

350 assert 'id="carousel-navbar"' in report.__str__()