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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1"""
2Test the design_matrix utilities.
4Note that the tests just looks whether the data produces has correct dimension,
5not whether it is exact
6"""
8from pathlib import Path
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)
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)
28from ._testing import (
29 block_paradigm,
30 design_with_negative_onsets,
31 modulated_block_paradigm,
32 modulated_event_paradigm,
33 spm_paradigm,
34)
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)
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
74@pytest.fixture
75def n_frames():
76 return 128
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)
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]
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)
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
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))
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)
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 )
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"]
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()
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)
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)
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)
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()
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))
276 onset = events.onset[events.trial_type == "c2"].astype(int)
277 assert_array_almost_equal(X[onset + 4, 11], np.ones(3))
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)
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()
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])
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 )
342 # check that the drifts are not constant
343 assert np.any(np.diff(X[:, -2]) != 0)
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 )
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 )
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 )
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())
405def test_events_as_path(n_frames, tmp_path):
406 events = basic_paradigm()
407 frame_times = np.linspace(0, n_frames - 1, n_frames)
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))
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))
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
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)
443 _, matrix, names = check_design_matrix(DM)
444 _, matrix_, names_ = check_design_matrix(DM2)
445 assert_almost_equal(matrix, matrix_)
446 assert names == names_
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)
461 spm_design_matrix = DESIGN_MATRIX[array]
463 assert ((spm_design_matrix - matrix) ** 2).sum() / (
464 spm_design_matrix**2
465 ).sum() < 0.1
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
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 )