Coverage for nilearn/glm/tests/test_design_matrix.py: 0%

179 statements  

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

1""" 

2Test the design_matrix utilities. 

3 

4Note that the tests just looks whether the data produces has correct dimension, 

5not whether it is exact 

6""" 

7 

8from pathlib import Path 

9 

10import numpy as np 

11import pandas as pd 

12import pytest 

13from numpy.testing import ( 

14 assert_almost_equal, 

15 assert_array_almost_equal, 

16 assert_array_equal, 

17) 

18 

19from nilearn._utils.data_gen import basic_paradigm 

20from nilearn.glm.first_level.design_matrix import ( 

21 _convolve_regressors, 

22 check_design_matrix, 

23 create_cosine_drift, 

24 make_first_level_design_matrix, 

25 make_second_level_design_matrix, 

26) 

27 

28from ._testing import ( 

29 block_paradigm, 

30 design_with_negative_onsets, 

31 modulated_block_paradigm, 

32 modulated_event_paradigm, 

33 spm_paradigm, 

34) 

35 

36# load the spm file to test cosine basis 

37my_path = Path(__file__).resolve().parent 

38full_path_design_matrix_file = my_path / "spm_dmtx.npz" 

39DESIGN_MATRIX = np.load(full_path_design_matrix_file) 

40 

41 

42def design_matrix_light( 

43 frame_times, 

44 events=None, 

45 hrf_model="glover", 

46 drift_model="cosine", 

47 high_pass=0.01, 

48 drift_order=1, 

49 fir_delays=None, 

50 add_regs=None, 

51 add_reg_names=None, 

52 min_onset=-24, 

53): 

54 """Perform same as make_first_level_design_matrix, \ 

55 but only returns the computed matrix and associated name. 

56 """ 

57 fir_delays = fir_delays or [0] 

58 dmtx = make_first_level_design_matrix( 

59 frame_times, 

60 events, 

61 hrf_model, 

62 drift_model, 

63 high_pass, 

64 drift_order, 

65 fir_delays, 

66 add_regs, 

67 add_reg_names, 

68 min_onset, 

69 ) 

70 _, matrix, names = check_design_matrix(dmtx) 

71 return matrix, names 

72 

73 

74@pytest.fixture 

75def n_frames(): 

76 return 128 

77 

78 

79@pytest.fixture 

80def frame_times(n_frames): 

81 t_r = 1.0 

82 return np.linspace(0, (n_frames - 1) * t_r, n_frames) 

83 

84 

85def test_cosine_drift(): 

86 # add something so that when the tests are launched 

87 # from a different directory 

88 spm_drifts = DESIGN_MATRIX["cosbf_dt_1_nt_20_hcut_0p1"] 

89 frame_times = np.arange(20) 

90 high_pass_frequency = 0.1 

91 nilearn_drifts = create_cosine_drift(high_pass_frequency, frame_times) 

92 assert_almost_equal(spm_drifts[:, 1:], nilearn_drifts[:, :-2]) 

93 # nilearn_drifts is placing the constant at the end [:, : - 1] 

94 

95 

96def test_design_matrix_no_experimental_paradigm(frame_times): 

97 # Test design matrix creation when no experimental paradigm is provided 

98 _, X, names = check_design_matrix( 

99 make_first_level_design_matrix( 

100 frame_times, drift_model="polynomial", drift_order=3 

101 ) 

102 ) 

103 assert len(names) == 4 

104 x = np.linspace(-0.5, 0.5, len(frame_times)) 

105 assert_almost_equal(X[:, 0], x) 

106 

107 

108def test_design_matrix_regressors_provided_manually(rng, frame_times): 

109 # test design matrix creation when regressors are provided manually 

110 ax = rng.standard_normal(size=(len(frame_times), 4)) 

111 _, X, names = check_design_matrix( 

112 make_first_level_design_matrix( 

113 frame_times, drift_model="polynomial", drift_order=3, add_regs=ax 

114 ) 

115 ) 

116 assert_almost_equal(X[:, 0], ax[:, 0]) 

117 assert len(names) == 8 

118 assert X.shape[1] == 8 

119 

120 # with pandas Dataframe 

121 axdf = pd.DataFrame(ax) 

122 _, X1, names = check_design_matrix( 

123 make_first_level_design_matrix( 

124 frame_times, drift_model="polynomial", drift_order=3, add_regs=axdf 

125 ) 

126 ) 

127 assert_almost_equal(X1[:, 0], ax[:, 0]) 

128 assert_array_equal(names[:4], np.arange(4)) 

129 

130 

131def test_design_matrix_regressors_provided_manually_errors(rng, frame_times): 

132 ax = rng.standard_normal(size=(len(frame_times) - 1, 4)) 

133 with pytest.raises( 

134 AssertionError, 

135 match="Incorrect specification of additional regressors:.", 

136 ): 

137 make_first_level_design_matrix(frame_times, add_regs=ax) 

138 

139 ax = rng.standard_normal(size=(len(frame_times), 4)) 

140 with pytest.raises( 

141 ValueError, match="Incorrect number of additional regressor names." 

142 ): 

143 make_first_level_design_matrix( 

144 frame_times, add_regs=ax, add_reg_names="" 

145 ) 

146 

147 

148def test_convolve_regressors(frame_times): 

149 # tests for convolve_regressors helper function 

150 _, names = _convolve_regressors(basic_paradigm(), "glover", frame_times) 

151 assert names == ["c0", "c1", "c2"] 

152 

153 

154def test_design_matrix_basic_paradigm_glover_hrf(frame_times): 

155 X, _ = design_matrix_light( 

156 frame_times, 

157 events=basic_paradigm(), 

158 hrf_model="glover", 

159 drift_model="polynomial", 

160 drift_order=3, 

161 ) 

162 assert (X[:, -1] == 1).all() 

163 assert (np.isnan(X) == 0).all() 

164 

165 

166@pytest.mark.parametrize( 

167 "events, hrf_model, drift_model, drift_order, high_pass, n_regressors", 

168 [ 

169 (basic_paradigm(), "glover", None, 1, 0.01, 4), 

170 ( 

171 basic_paradigm(), 

172 "glover", 

173 "cosine", 

174 1, 

175 1.0 / 63, 

176 8, 

177 ), 

178 (basic_paradigm(), "glover + derivative", "polynomial", 3, 0.01, 10), 

179 (block_paradigm(), "glover", "polynomial", 1, 0.01, 5), 

180 (block_paradigm(), "glover", "polynomial", 3, 0.01, 7), 

181 (block_paradigm(), "glover + derivative", "polynomial", 3, 0.01, 10), 

182 ], 

183) 

184def test_design_matrix( 

185 frame_times, 

186 n_frames, 

187 events, 

188 hrf_model, 

189 drift_model, 

190 drift_order, 

191 high_pass, 

192 n_regressors, 

193): 

194 X, names = design_matrix_light( 

195 frame_times, 

196 events=events, 

197 hrf_model=hrf_model, 

198 drift_model=drift_model, 

199 drift_order=drift_order, 

200 high_pass=high_pass, 

201 ) 

202 assert len(names) == n_regressors 

203 assert X.shape == (n_frames, n_regressors) 

204 

205 

206def test_design_matrix_basic_paradigm_and_extra_regressors(rng, frame_times): 

207 # basic test based on basic_paradigm, plus user supplied regressors 

208 ax = rng.standard_normal(size=(len(frame_times), 4)) 

209 X, names = design_matrix_light( 

210 frame_times, 

211 events=basic_paradigm(), 

212 hrf_model="glover", 

213 drift_model="polynomial", 

214 drift_order=3, 

215 add_regs=ax, 

216 ) 

217 assert len(names) == 11 

218 assert X.shape[1] == 11 

219 # Check that additional regressors are put at the right place 

220 assert_almost_equal(X[:, 3:7], ax) 

221 

222 

223@pytest.mark.parametrize( 

224 "fir_delays, n_regressors", [(None, 7), (range(1, 5), 16)] 

225) 

226def test_design_matrix_fir_basic_paradigm( 

227 frame_times, fir_delays, n_regressors 

228): 

229 # basic test based on basic_paradigm and FIR 

230 X, names = design_matrix_light( 

231 frame_times, 

232 events=basic_paradigm(), 

233 hrf_model="FIR", 

234 drift_model="polynomial", 

235 drift_order=3, 

236 fir_delays=fir_delays, 

237 ) 

238 assert len(names) == n_regressors 

239 assert X.shape == (len(frame_times), n_regressors) 

240 

241 

242def test_design_matrix_fir_block(frame_times): 

243 # test FIR models on block designs 

244 bp = block_paradigm() 

245 X, _ = design_matrix_light( 

246 frame_times, 

247 bp, 

248 hrf_model="fir", 

249 drift_model=None, 

250 fir_delays=range(4), 

251 ) 

252 idx = bp["onset"][bp["trial_type"] == 1].astype(int) 

253 assert X.shape == (len(frame_times), 13) 

254 assert (X[idx, 4] == 1).all() 

255 assert (X[idx + 1, 5] == 1).all() 

256 assert (X[idx + 2, 6] == 1).all() 

257 assert (X[idx + 3, 7] == 1).all() 

258 

259 

260def test_design_matrix_fir_column_1_3_and_11(frame_times): 

261 # Check that 1rst, 3rd and 11th of FIR design matrix are OK 

262 events = basic_paradigm() 

263 hrf_model = "FIR" 

264 X, _ = design_matrix_light( 

265 frame_times, 

266 events, 

267 hrf_model=hrf_model, 

268 drift_model="polynomial", 

269 drift_order=3, 

270 fir_delays=range(1, 5), 

271 ) 

272 onset = events.onset[events.trial_type == "c0"].astype(int) 

273 assert_array_almost_equal(X[onset + 1, 0], np.ones(3)) 

274 assert_array_almost_equal(X[onset + 3, 2], np.ones(3)) 

275 

276 onset = events.onset[events.trial_type == "c2"].astype(int) 

277 assert_array_almost_equal(X[onset + 4, 11], np.ones(3)) 

278 

279 

280def test_design_matrix_fir_time_shift(frame_times): 

281 # Check that the first column of FIR design matrix is OK after a 1/2 

282 # time shift 

283 t_r = 1.0 

284 frame_times = frame_times + t_r / 2 

285 events = basic_paradigm() 

286 hrf_model = "FIR" 

287 X, _ = design_matrix_light( 

288 frame_times, 

289 events, 

290 hrf_model=hrf_model, 

291 drift_model="polynomial", 

292 drift_order=3, 

293 fir_delays=range(1, 5), 

294 ) 

295 ct = events.onset[events.trial_type == "c0"].astype(int) 

296 assert np.all(X[ct + 1, 0] > 0.5) 

297 

298 

299@pytest.mark.parametrize( 

300 "events, idx_offset", 

301 [(modulated_event_paradigm(), 1), (modulated_block_paradigm(), 3)], 

302) 

303def test_design_matrix_scaling(events, idx_offset, frame_times): 

304 X, _ = design_matrix_light( 

305 frame_times, 

306 events=events, 

307 hrf_model="glover", 

308 drift_model="polynomial", 

309 drift_order=3, 

310 ) 

311 idx = events.onset[events.trial_type == "c0"].astype(int) 

312 ct = idx + idx_offset 

313 assert (X[ct, 0] > 0).all() 

314 

315 

316def test_design_matrix_scaling_fir_model(frame_times): 

317 # Test the effect of scaling on a FIR model 

318 events = modulated_event_paradigm() 

319 hrf_model = "FIR" 

320 X, _ = design_matrix_light( 

321 frame_times, 

322 events, 

323 hrf_model=hrf_model, 

324 drift_model="polynomial", 

325 drift_order=3, 

326 fir_delays=range(1, 5), 

327 ) 

328 idx = events.onset[events.trial_type == 0].astype(int) 

329 assert_array_equal(X[idx + 1, 0], X[idx + 2, 1]) 

330 

331 

332def test_design_matrix20(n_frames): 

333 # Test for commit 10662f7 

334 frame_times = np.arange( 

335 0, n_frames 

336 ) # was 127 in old version of create_cosine_drift 

337 events = modulated_event_paradigm() 

338 X, _ = design_matrix_light( 

339 frame_times, events, hrf_model="glover", drift_model="cosine" 

340 ) 

341 

342 # check that the drifts are not constant 

343 assert np.any(np.diff(X[:, -2]) != 0) 

344 

345 

346def test_design_matrix_repeated_name_in_user_regressors(rng, frame_times): 

347 # basic test on repeated names of user supplied regressors 

348 events = basic_paradigm() 

349 hrf_model = "glover" 

350 ax = rng.standard_normal(size=(len(frame_times), 4)) 

351 with pytest.raises( 

352 ValueError, match="Design matrix columns do not have unique names" 

353 ): 

354 design_matrix_light( 

355 frame_times, 

356 events, 

357 hrf_model=hrf_model, 

358 drift_model="polynomial", 

359 drift_order=3, 

360 add_regs=ax, 

361 add_reg_names=["aha"] * ax.shape[1], 

362 ) 

363 

364 

365def test_oversampling(n_frames): 

366 events = basic_paradigm() 

367 frame_times = np.linspace(0, n_frames - 1, n_frames) 

368 X1 = make_first_level_design_matrix(frame_times, events, drift_model=None) 

369 X2 = make_first_level_design_matrix( 

370 frame_times, events, drift_model=None, oversampling=50 

371 ) 

372 X3 = make_first_level_design_matrix( 

373 frame_times, events, drift_model=None, oversampling=10 

374 ) 

375 

376 # oversampling = 50 by default so X2 = X1, X3 \neq X1, X3 close to X2 

377 assert_almost_equal(X1.to_numpy(), X2.to_numpy()) 

378 assert_almost_equal(X2.to_numpy(), X3.to_numpy(), 0) 

379 assert ( 

380 np.linalg.norm(X2.to_numpy() - X3.to_numpy()) 

381 / np.linalg.norm(X2.to_numpy()) 

382 > 1.0e-4 

383 ) 

384 

385 # fir model, oversampling is forced to 1 

386 X4 = make_first_level_design_matrix( 

387 frame_times, 

388 events, 

389 hrf_model="fir", 

390 drift_model=None, 

391 fir_delays=range(4), 

392 oversampling=1, 

393 ) 

394 X5 = make_first_level_design_matrix( 

395 frame_times, 

396 events, 

397 hrf_model="fir", 

398 drift_model=None, 

399 fir_delays=range(4), 

400 oversampling=10, 

401 ) 

402 assert_almost_equal(X4.to_numpy(), X5.to_numpy()) 

403 

404 

405def test_events_as_path(n_frames, tmp_path): 

406 events = basic_paradigm() 

407 frame_times = np.linspace(0, n_frames - 1, n_frames) 

408 

409 events_file = tmp_path / "design.csv" 

410 events.to_csv(events_file) 

411 make_first_level_design_matrix(frame_times, events=events_file) 

412 make_first_level_design_matrix(frame_times, events=str(events_file)) 

413 

414 events_file = tmp_path / "design.tsv" 

415 events.to_csv(events_file, sep="\t") 

416 make_first_level_design_matrix(frame_times, events=events_file) 

417 make_first_level_design_matrix(frame_times, events=str(events_file)) 

418 

419 

420def test_high_pass(n_frames): 

421 """Test that high-pass values lead to reasonable design matrices.""" 

422 t_r = 2.0 

423 frame_times = np.arange(0, t_r * n_frames, t_r) 

424 X = make_first_level_design_matrix( 

425 frame_times, drift_model="Cosine", high_pass=1.0 

426 ) 

427 assert X.shape[1] == n_frames 

428 

429 

430def test_csv_io(tmp_path, frame_times): 

431 # test the csv io on design matrices 

432 DM = make_first_level_design_matrix( 

433 frame_times, 

434 events=modulated_event_paradigm(), 

435 hrf_model="glover", 

436 drift_model="polynomial", 

437 drift_order=3, 

438 ) 

439 path = tmp_path / "design_matrix.csv" 

440 DM.to_csv(path) 

441 DM2 = pd.read_csv(path, index_col=0) 

442 

443 _, matrix, names = check_design_matrix(DM) 

444 _, matrix_, names_ = check_design_matrix(DM2) 

445 assert_almost_equal(matrix, matrix_) 

446 assert names == names_ 

447 

448 

449@pytest.mark.parametrize( 

450 "block_duration, array", [(1, "arr_0"), (10, "arr_1")] 

451) 

452def test_compare_design_matrix_to_spm(block_duration, array): 

453 # Check that the nilearn design matrix is close enough to the SPM one 

454 # (it cannot be identical, because the hrf shape is different) 

455 events, frame_times = spm_paradigm(block_duration=block_duration) 

456 X1 = make_first_level_design_matrix( 

457 frame_times, events, drift_model=None, hrf_model="spm" 

458 ) 

459 _, matrix, _ = check_design_matrix(X1) 

460 

461 spm_design_matrix = DESIGN_MATRIX[array] 

462 

463 assert ((spm_design_matrix - matrix) ** 2).sum() / ( 

464 spm_design_matrix**2 

465 ).sum() < 0.1 

466 

467 

468def test_create_second_level_design(): 

469 subjects_label = ["02", "01"] # change order to test right output order 

470 regressors = [["01", 0.1], ["02", 0.75]] 

471 regressors = pd.DataFrame(regressors, columns=["subject_label", "f1"]) 

472 design = make_second_level_design_matrix(subjects_label, regressors) 

473 expected_design = np.array([[0.75, 1.0], [0.1, 1.0]]) 

474 assert_array_equal(design, expected_design) 

475 assert len(design.columns) == 2 

476 assert len(design) == 2 

477 

478 

479def test_designs_with_negative_onsets_warning(frame_times): 

480 with pytest.warns( 

481 UserWarning, 

482 match="Some stimulus onsets are earlier than", 

483 ): 

484 make_first_level_design_matrix( 

485 events=design_with_negative_onsets(), frame_times=frame_times 

486 )