Coverage for nilearn/glm/first_level/first_level.py: 8%

624 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-20 10:58 +0200

1"""Contains the GLM and contrast classes that are meant to be the main \ 

2objects of fMRI data analyses. 

3 

4Author: Bertrand Thirion, Martin Perez-Guevara, 2016 

5 

6""" 

7 

8from __future__ import annotations 

9 

10import csv 

11import inspect 

12import time 

13from collections.abc import Iterable 

14from pathlib import Path 

15from warnings import warn 

16 

17import numpy as np 

18import pandas as pd 

19from joblib import Memory, Parallel, delayed 

20from nibabel import Nifti1Image 

21from scipy.linalg import toeplitz 

22from sklearn.cluster import KMeans 

23from sklearn.utils.estimator_checks import check_is_fitted 

24 

25from nilearn._utils import fill_doc, logger 

26from nilearn._utils.cache_mixin import check_memory 

27from nilearn._utils.glm import check_and_load_tables 

28from nilearn._utils.logger import find_stack_level 

29from nilearn._utils.masker_validation import ( 

30 check_compatibility_mask_and_images, 

31 check_embedded_masker, 

32) 

33from nilearn._utils.niimg_conversions import check_niimg 

34from nilearn._utils.param_validation import ( 

35 check_params, 

36 check_run_sample_masks, 

37) 

38from nilearn.datasets import load_fsaverage 

39from nilearn.glm._base import BaseGLM 

40from nilearn.glm.contrasts import ( 

41 compute_fixed_effect_contrast, 

42 expression_to_contrast_vector, 

43) 

44from nilearn.glm.first_level.design_matrix import ( 

45 make_first_level_design_matrix, 

46) 

47from nilearn.glm.regression import ( 

48 ARModel, 

49 OLSModel, 

50 RegressionResults, 

51 SimpleRegressionResults, 

52) 

53from nilearn.image import get_data 

54from nilearn.interfaces.bids import get_bids_files, parse_bids_filename 

55from nilearn.interfaces.bids.query import ( 

56 infer_repetition_time_from_dataset, 

57 infer_slice_timing_start_time_from_dataset, 

58) 

59from nilearn.interfaces.bids.utils import bids_entities, check_bids_label 

60from nilearn.interfaces.fmriprep.load_confounds import load_confounds 

61from nilearn.maskers import SurfaceMasker 

62from nilearn.surface import SurfaceImage 

63from nilearn.typing import NiimgLike 

64 

65 

66def mean_scaling(Y, axis=0): 

67 """Scaling of the data to have percent of baseline change \ 

68 along the specified axis. 

69 

70 Parameters 

71 ---------- 

72 Y : array of shape (n_time_points, n_voxels) 

73 The input data. 

74 

75 axis : :obj:`int`, default=0 

76 Axis along which the scaling mean should be calculated. 

77 

78 Returns 

79 ------- 

80 Y : array of shape (n_time_points, n_voxels), 

81 The data after mean-scaling, de-meaning and multiplication by 100. 

82 

83 mean : array of shape (n_voxels,) 

84 The data mean. 

85 

86 """ 

87 mean = Y.mean(axis=axis) 

88 if (mean == 0).any(): 

89 warn( 

90 "Mean values of 0 observed. " 

91 "The data have probably been centered." 

92 "Scaling might not work as expected", 

93 UserWarning, 

94 stacklevel=find_stack_level(), 

95 ) 

96 mean = np.maximum(mean, 1) 

97 Y = 100 * (Y / mean - 1) 

98 return Y, mean 

99 

100 

101def _ar_model_fit(X, val, Y): 

102 """Wrap fit method of ARModel to allow joblib parallelization.""" 

103 return ARModel(X, val).fit(Y) 

104 

105 

106def _yule_walker(x, order): 

107 """Compute Yule-Walker (adapted from MNE and statsmodels). 

108 

109 Operates along the last axis of x. 

110 """ 

111 if order < 1: 

112 raise ValueError("AR order must be positive") 

113 if type(order) is not int: 

114 raise TypeError("AR order must be an integer") 

115 if x.ndim < 1: 

116 raise TypeError("Input data must have at least 1 dimension") 

117 

118 denom = x.shape[-1] - np.arange(order + 1) 

119 n = np.prod(np.array(x.shape[:-1], int)) 

120 r = np.zeros((n, order + 1), np.float64) 

121 y = x - x.mean() 

122 y.shape = (n, x.shape[-1]) # inplace 

123 r[:, 0] += (y[:, np.newaxis, :] @ y[:, :, np.newaxis])[:, 0, 0] 

124 for k in range(1, order + 1): 

125 r[:, k] += (y[:, np.newaxis, 0:-k] @ y[:, k:, np.newaxis])[:, 0, 0] 

126 r /= denom * x.shape[-1] 

127 rt = np.array([toeplitz(rr[:-1]) for rr in r], np.float64) 

128 

129 # extra dimension added to r for compatibility with numpy <2 and >2 

130 # see https://numpy.org/devdocs/release/2.0.0-notes.html 

131 # section removed-ambiguity-when-broadcasting-in-np-solve 

132 rho = np.linalg.solve(rt, r[:, 1:, None])[..., 0] 

133 

134 rho.shape = x.shape[:-1] + (order,) 

135 return rho 

136 

137 

138@fill_doc 

139def run_glm( 

140 Y, X, noise_model="ar1", bins=100, n_jobs=1, verbose=0, random_state=None 

141): 

142 """:term:`GLM` fit for an :term:`fMRI` data matrix. 

143 

144 Parameters 

145 ---------- 

146 Y : array of shape (n_time_points, n_voxels) 

147 The :term:`fMRI` data. 

148 

149 X : array of shape (n_time_points, n_regressors) 

150 The design matrix. 

151 

152 noise_model : {'ar(N)', 'ols'}, default='ar1' 

153 The temporal variance model. 

154 To specify the order of an autoregressive model place the 

155 order after the characters `ar`, for example to specify a third order 

156 model use `ar3`. 

157 

158 bins : :obj:`int`, default=100 

159 Maximum number of discrete bins for the AR coef histogram. 

160 If an autoregressive model with order greater than one is specified 

161 then adaptive quantification is performed and the coefficients 

162 will be clustered via K-means with `bins` number of clusters. 

163 

164 n_jobs : :obj:`int`, default=1 

165 The number of CPUs to use to do the computation. -1 means 

166 'all CPUs'. 

167 

168 %(verbose0)s 

169 

170 random_state : :obj:`int` or numpy.random.RandomState, default=None 

171 Random state seed to sklearn.cluster.KMeans for autoregressive models 

172 of order at least 2 ('ar(N)' with n >= 2). 

173 

174 .. versionadded:: 0.9.1 

175 

176 Returns 

177 ------- 

178 labels : array of shape (n_voxels,), 

179 A map of values on voxels used to identify the corresponding model. 

180 

181 results : :obj:`dict`, 

182 Keys correspond to the different labels values 

183 values are RegressionResults instances corresponding to the voxels. 

184 

185 """ 

186 acceptable_noise_models = ["ols", "arN"] 

187 if (noise_model[:2] != "ar") and (noise_model != "ols"): 

188 raise ValueError( 

189 f"Acceptable noise models are {acceptable_noise_models}. " 

190 f"You provided 'noise_model={noise_model}'." 

191 ) 

192 if Y.shape[0] != X.shape[0]: 

193 raise ValueError( 

194 "The number of rows of Y " 

195 "should match the number of rows of X.\n" 

196 f"You provided X with shape {X.shape} " 

197 f"and Y with shape {Y.shape}." 

198 ) 

199 

200 # Create the model 

201 ols_result = OLSModel(X).fit(Y) 

202 

203 if noise_model[:2] == "ar": 

204 err_msg = ( 

205 "AR order must be a positive integer specified as arN, " 

206 "where N is an integer. E.g. ar3. " 

207 f"You provided {noise_model}." 

208 ) 

209 try: 

210 ar_order = int(noise_model[2:]) 

211 except ValueError: 

212 raise ValueError(err_msg) 

213 

214 # compute the AR coefficients 

215 ar_coef_ = _yule_walker(ols_result.residuals.T, ar_order) 

216 del ols_result 

217 if len(ar_coef_[0]) == 1: 

218 ar_coef_ = ar_coef_[:, 0] 

219 

220 # Either bin the AR1 coefs or cluster ARN coefs 

221 if ar_order == 1: 

222 for idx in range(len(ar_coef_)): 

223 ar_coef_[idx] = (ar_coef_[idx] * bins).astype(int) * 1.0 / bins 

224 labels = np.array([str(val) for val in ar_coef_]) 

225 else: # AR(N>1) case 

226 n_clusters = np.min([bins, Y.shape[1]]) 

227 kmeans = KMeans( 

228 n_clusters=n_clusters, n_init=10, random_state=random_state 

229 ).fit(ar_coef_) 

230 ar_coef_ = kmeans.cluster_centers_[kmeans.labels_] 

231 

232 # Create a set of rounded values for the labels with _ between 

233 # each coefficient 

234 cluster_labels = kmeans.cluster_centers_.copy() 

235 cluster_labels = np.array( 

236 ["_".join(map(str, np.round(a, 2))) for a in cluster_labels] 

237 ) 

238 # Create labels and coef per voxel 

239 labels = np.array([cluster_labels[i] for i in kmeans.labels_]) 

240 

241 unique_labels = np.unique(labels) 

242 results = {} 

243 

244 # Fit the AR model according to current AR(N) estimates 

245 ar_result = Parallel(n_jobs=n_jobs, verbose=verbose)( 

246 delayed(_ar_model_fit)( 

247 X, ar_coef_[labels == val][0], Y[:, labels == val] 

248 ) 

249 for val in unique_labels 

250 ) 

251 

252 # Converting the key to a string is required for AR(N>1) cases 

253 results = dict(zip(unique_labels, ar_result)) 

254 del unique_labels 

255 del ar_result 

256 

257 else: 

258 labels = np.zeros(Y.shape[1]) 

259 results = {0.0: ols_result} 

260 

261 return labels, results 

262 

263 

264def _check_trial_type(events): 

265 """Check that the event files contain a "trial_type" column. 

266 

267 Parameters 

268 ---------- 

269 events : :obj:`list` of :obj:`str` or :obj:`pathlib.Path``. 

270 A list of paths of events.tsv files. 

271 

272 """ 

273 file_names = [] 

274 

275 for event_ in events: 

276 events_df = pd.read_csv(event_, sep="\t") 

277 if "trial_type" not in events_df.columns: 

278 file_names.append(Path(event_).name) 

279 

280 if file_names: 

281 file_names = "\n -".join(file_names) 

282 warn( 

283 f"No column named 'trial_type' found in:{file_names}.\n " 

284 "All rows in those files will be treated " 

285 "as if they are instances of same experimental condition.\n" 

286 "If there is a column in the dataframe " 

287 "corresponding to trial information, " 

288 "consider renaming it to 'trial_type'.", 

289 stacklevel=find_stack_level(), 

290 ) 

291 

292 

293@fill_doc 

294class FirstLevelModel(BaseGLM): 

295 """Implement the General Linear Model for single run :term:`fMRI` data. 

296 

297 Parameters 

298 ---------- 

299 t_r : :obj:`float` or None, default=None 

300 This parameter indicates :term:`repetition times<TR>` 

301 of the experimental runs. 

302 In seconds. It is necessary to correctly consider times in the design 

303 matrix. This parameter is also passed to :func:`nilearn.signal.clean`. 

304 Please see the related documentation for details. 

305 

306 .. warning:: 

307 

308 This parameter is ignored by fit() if design matrices 

309 are passed at fit time. 

310 

311 slice_time_ref : :obj:`float`, default=0.0 

312 This parameter indicates the time of the reference slice used in the 

313 slice timing preprocessing step of the experimental runs. 

314 It is expressed as a fraction of the ``t_r`` (repetition time), 

315 so it can have values between 0. and 1. 

316 

317 .. warning:: 

318 

319 This parameter is ignored by fit() if design matrices 

320 are passed at fit time. 

321 

322 %(hrf_model)s 

323 Default='glover'. 

324 

325 .. warning:: 

326 

327 This parameter is ignored by fit() if design matrices 

328 are passed at fit time. 

329 

330 drift_model : :obj:`str`, default='cosine' 

331 This parameter specifies the desired drift model for the design 

332 matrices. It can be 'polynomial', 'cosine' or None. 

333 

334 .. warning:: 

335 

336 This parameter is ignored by fit() if design matrices 

337 are passed at fit time. 

338 

339 high_pass : :obj:`float`, default=0.01 

340 This parameter specifies the cut frequency of the high-pass filter in 

341 Hz for the design matrices. Used only if drift_model is 'cosine'. 

342 

343 .. warning:: 

344 

345 This parameter is ignored by fit() if design matrices 

346 are passed at fit time. 

347 

348 drift_order : :obj:`int`, default=1 

349 This parameter specifies the order of the drift model (in case it is 

350 polynomial) for the design matrices. 

351 

352 .. warning:: 

353 

354 This parameter is ignored by fit() if design matrices 

355 are passed at fit time. 

356 

357 fir_delays : array of shape(n_onsets), :obj:`list` or None, default=None 

358 Will be set to ``[0]`` if ``None`` is passed. 

359 In case of :term:`FIR` design, 

360 yields the array of delays used in the :term:`FIR` model, 

361 in scans. 

362 

363 .. warning:: 

364 

365 This parameter is ignored by fit() if design matrices 

366 are passed at fit time. 

367 

368 min_onset : :obj:`float`, default=-24 

369 This parameter specifies the minimal onset relative to the design 

370 (in seconds). Events that start before (slice_time_ref * t_r + 

371 min_onset) are not considered. 

372 

373 .. warning:: 

374 

375 This parameter is ignored by fit() if design matrices 

376 are passed at fit time. 

377 

378 mask_img : Niimg-like, NiftiMasker, :obj:`~nilearn.surface.SurfaceImage`,\ 

379 :obj:`~nilearn.maskers.SurfaceMasker`, False or \ 

380 None, default=None 

381 Mask to be used on data. 

382 If an instance of masker is passed, then its mask will be used. 

383 If None is passed, the mask will be computed automatically 

384 by a NiftiMasker 

385 or :obj:`~nilearn.maskers.SurfaceMasker` with default parameters. 

386 If False is given then the data will not be masked. 

387 In the case of surface analysis, passing None or False will lead to 

388 no masking. 

389 

390 %(target_affine)s 

391 

392 .. note:: 

393 This parameter is passed to :func:`nilearn.image.resample_img`. 

394 

395 %(target_shape)s 

396 

397 .. note:: 

398 This parameter is passed to :func:`nilearn.image.resample_img`. 

399 

400 %(smoothing_fwhm)s 

401 

402 %(memory)s 

403 

404 %(memory_level)s 

405 

406 standardize : :obj:`bool`, default=False 

407 If standardize is True, the time-series are centered and normed: 

408 their variance is put to 1 in the time dimension. 

409 

410 signal_scaling : False, :obj:`int` or (int, int), default=0 

411 If not False, fMRI signals are 

412 scaled to the mean value of scaling_axis given, 

413 which can be 0, 1 or (0, 1). 

414 0 refers to mean scaling each voxel with respect to time, 

415 1 refers to mean scaling each time point with respect to all voxels & 

416 (0, 1) refers to scaling with respect to voxels and time, 

417 which is known as grand mean scaling. 

418 Incompatible with standardize (standardize=False is enforced when 

419 signal_scaling is not False). 

420 

421 noise_model : {'ar1', 'ols'}, default='ar1' 

422 The temporal variance model. 

423 

424 %(verbose)s 

425 If 1 prints progress by computation of 

426 each run. If 2 prints timing details of masker and GLM. If 3 

427 prints masker computation details. 

428 

429 %(n_jobs)s 

430 

431 minimize_memory : :obj:`bool`, default=True 

432 Gets rid of some variables on the model fit results that are not 

433 necessary for contrast computation and would only be useful for 

434 further inspection of model details. This has an important impact 

435 on memory consumption. 

436 

437 subject_label : :obj:`str`, optional 

438 This id will be used to identify a `FirstLevelModel` when passed to 

439 a `SecondLevelModel` object. 

440 

441 random_state : :obj:`int` or numpy.random.RandomState, default=None. 

442 Random state seed to sklearn.cluster.KMeans 

443 for autoregressive models 

444 of order at least 2 ('ar(N)' with n >= 2). 

445 

446 .. versionadded:: 0.9.1 

447 

448 Attributes 

449 ---------- 

450 labels_ : array of shape (n_voxels,), 

451 a map of values on voxels used to identify the corresponding model 

452 

453 results_ : :obj:`dict`, 

454 with keys corresponding to the different labels values. 

455 Values are SimpleRegressionResults corresponding to the voxels, 

456 if minimize_memory is True, 

457 RegressionResults if minimize_memory is False 

458 

459 """ 

460 

461 def __str__(self): 

462 return "First Level Model" 

463 

464 def __init__( 

465 self, 

466 t_r=None, 

467 slice_time_ref=0.0, 

468 hrf_model="glover", 

469 drift_model="cosine", 

470 high_pass=0.01, 

471 drift_order=1, 

472 fir_delays=None, 

473 min_onset=-24, 

474 mask_img=None, 

475 target_affine=None, 

476 target_shape=None, 

477 smoothing_fwhm=None, 

478 memory=None, 

479 memory_level=1, 

480 standardize=False, 

481 signal_scaling=0, 

482 noise_model="ar1", 

483 verbose=0, 

484 n_jobs=1, 

485 minimize_memory=True, 

486 subject_label=None, 

487 random_state=None, 

488 ): 

489 # design matrix parameters 

490 self.t_r = t_r 

491 self.slice_time_ref = slice_time_ref 

492 self.hrf_model = hrf_model 

493 self.drift_model = drift_model 

494 self.high_pass = high_pass 

495 self.drift_order = drift_order 

496 self.fir_delays = fir_delays 

497 self.min_onset = min_onset 

498 

499 # glm parameters 

500 self.mask_img = mask_img 

501 self.target_affine = target_affine 

502 self.target_shape = target_shape 

503 self.smoothing_fwhm = smoothing_fwhm 

504 self.memory = memory 

505 self.memory_level = memory_level 

506 self.standardize = standardize 

507 self.signal_scaling = signal_scaling 

508 

509 self.noise_model = noise_model 

510 self.verbose = verbose 

511 self.n_jobs = n_jobs 

512 self.minimize_memory = minimize_memory 

513 

514 # attributes 

515 self.subject_label = subject_label 

516 self.random_state = random_state 

517 

518 def _check_fit_inputs( 

519 self, 

520 run_imgs, 

521 events, 

522 confounds, 

523 sample_masks, 

524 design_matrices, 

525 ): 

526 """Run input validation and ensure inputs are compatible.""" 

527 if not isinstance( 

528 run_imgs, (str, Path, Nifti1Image, SurfaceImage, list, tuple) 

529 ) or ( 

530 isinstance(run_imgs, (list, tuple)) 

531 and not all( 

532 isinstance(x, (*NiimgLike, SurfaceImage)) for x in run_imgs 

533 ) 

534 ): 

535 input_type = type(run_imgs) 

536 if isinstance(run_imgs, list): 

537 input_type = [type(x) for x in run_imgs] 

538 raise TypeError( 

539 "'run_imgs' must be a single instance / a list " 

540 "of any of the following:\n" 

541 "- string\n" 

542 "- pathlib.Path\n" 

543 "- NiftiImage\n" 

544 "- SurfaceImage\n" 

545 f"Got: {input_type}" 

546 ) 

547 

548 if not isinstance(run_imgs, (list, tuple)): 

549 run_imgs = [run_imgs] 

550 

551 if design_matrices is not None: 

552 # If design_matrices is provided, 

553 # throw warning for the attributes or parameters 

554 # that were provided at init or fit time 

555 # but that will be ignored 

556 # because they will not be used to generate a design matrix. 

557 parameters_to_ignore = [] 

558 if confounds is not None: 

559 parameters_to_ignore.append("confounds") 

560 if events is not None: 

561 parameters_to_ignore.append("events") 

562 if parameters_to_ignore: 

563 warn( 

564 "If design matrices are supplied, " 

565 f"{' and '.join(parameters_to_ignore)} will be ignored.", 

566 stacklevel=find_stack_level(), 

567 ) 

568 

569 # check with the default of __init__ 

570 attributes_to_ignore = [] 

571 attributes_used_in_des_mat_generation = [ 

572 "drift_model", 

573 "drift_order", 

574 "fir_delays", 

575 "high_pass", 

576 "hrf_model", 

577 "min_onset", 

578 "slice_time_ref", 

579 "t_r", 

580 ] 

581 tmp = dict(**inspect.signature(self.__init__).parameters) 

582 attributes_to_ignore.extend( 

583 [ 

584 k 

585 for k in attributes_used_in_des_mat_generation 

586 if getattr(self, k) != tmp[k].default 

587 ] 

588 ) 

589 

590 if attributes_to_ignore: 

591 warn( 

592 "If design matrices are supplied, " 

593 f"[{', '.join(attributes_to_ignore)}] will be ignored.", 

594 stacklevel=find_stack_level(), 

595 ) 

596 

597 design_matrices = _check_run_tables( 

598 run_imgs, design_matrices, "design_matrices" 

599 ) 

600 

601 else: 

602 if events is None: 

603 raise ValueError("events or design matrices must be provided") 

604 if self.t_r is None: 

605 raise ValueError( 

606 "t_r not given to FirstLevelModel object" 

607 " to compute design from events" 

608 ) 

609 

610 # Check that events and confounds files match number of runs 

611 # and can be loaded as DataFrame. 

612 _check_events_file_uses_tab_separators(events_files=events) 

613 events = _check_run_tables(run_imgs, events, "events") 

614 

615 if confounds is not None: 

616 confounds = _check_run_tables(run_imgs, confounds, "confounds") 

617 

618 if sample_masks is not None: 

619 sample_masks = check_run_sample_masks(len(run_imgs), sample_masks) 

620 

621 return ( 

622 run_imgs, 

623 events, 

624 confounds, 

625 sample_masks, 

626 design_matrices, 

627 ) 

628 

629 def _log( 

630 self, step, run_idx=None, n_runs=None, t0=None, time_in_second=None 

631 ): 

632 """Generate and log messages for different step of the model fit.""" 

633 if step == "progress": 

634 msg = self._report_progress(run_idx, n_runs, t0) 

635 elif step == "running": 

636 msg = "Performing GLM computation." 

637 elif step == "run_done": 

638 msg = f"GLM took {int(time_in_second)} seconds." 

639 elif step == "masking": 

640 msg = "Performing mask computation." 

641 elif step == "masking_done": 

642 msg = f"Masking took {int(time_in_second)} seconds." 

643 elif step == "done": 

644 msg = ( 

645 f"Computation of {n_runs} runs done " 

646 f"in {int(time_in_second)} seconds." 

647 ) 

648 

649 logger.log( 

650 msg, 

651 verbose=self.verbose, 

652 ) 

653 

654 def _report_progress(self, run_idx, n_runs, t0): 

655 remaining = "go take a coffee, a big one" 

656 if run_idx != 0: 

657 percent = float(run_idx) / n_runs 

658 percent = round(percent * 100, 2) 

659 dt = time.time() - t0 

660 # We use a max to avoid a division by zero 

661 remaining = (100.0 - percent) / max(0.01, percent) * dt 

662 remaining = f"{int(remaining)} seconds remaining" 

663 

664 return ( 

665 f"Computing run {run_idx + 1} out of {n_runs} runs ({remaining})." 

666 ) 

667 

668 def _fit_single_run(self, sample_masks, bins, run_img, run_idx): 

669 """Fit the model for a single and keep only the regression results.""" 

670 design = self.design_matrices_[run_idx] 

671 

672 sample_mask = None 

673 if sample_masks is not None: 

674 sample_mask = sample_masks[run_idx] 

675 design = design.iloc[sample_mask, :] 

676 self.design_matrices_[run_idx] = design 

677 

678 # Mask and prepare data for GLM 

679 self._log("masking") 

680 t_masking = time.time() 

681 Y = self.masker_.transform(run_img, sample_mask=sample_mask) 

682 del run_img # Delete unmasked image to save memory 

683 self._log("masking_done", time_in_second=time.time() - t_masking) 

684 

685 if self.signal_scaling is not False: 

686 Y, _ = mean_scaling(Y, self.signal_scaling) 

687 

688 if self.memory: 

689 mem_glm = self.memory.cache(run_glm, ignore=["n_jobs"]) 

690 else: 

691 mem_glm = run_glm 

692 

693 # compute GLM 

694 t_glm = time.time() 

695 self._log("running") 

696 

697 labels, results = mem_glm( 

698 Y, 

699 design.values, 

700 noise_model=self.noise_model, 

701 bins=bins, 

702 n_jobs=self.n_jobs, 

703 random_state=self.random_state, 

704 ) 

705 

706 self._log("run_done", time_in_second=time.time() - t_glm) 

707 

708 self.labels_.append(labels) 

709 

710 # We save memory if inspecting model details is not necessary 

711 if self.minimize_memory: 

712 results = { 

713 k: SimpleRegressionResults(v) for k, v in results.items() 

714 } 

715 self.results_.append(results) 

716 del Y 

717 

718 def _create_all_designs( 

719 self, run_imgs, events, confounds, design_matrices 

720 ): 

721 """Build experimental design of all runs.""" 

722 if design_matrices is not None: 

723 return design_matrices 

724 

725 design_matrices = [] 

726 

727 for run_idx, run_img in enumerate(run_imgs): 

728 if isinstance(run_img, SurfaceImage): 

729 n_scans = run_img.shape[1] 

730 else: 

731 run_img = check_niimg(run_img, ensure_ndim=4) 

732 n_scans = get_data(run_img).shape[3] 

733 

734 design = self._create_single_design( 

735 n_scans, events, confounds, run_idx 

736 ) 

737 

738 design_matrices.append(design) 

739 

740 return design_matrices 

741 

742 def _create_single_design(self, n_scans, events, confounds, run_idx): 

743 """Build experimental design of a single run. 

744 

745 Parameters 

746 ---------- 

747 n_scans: int 

748 

749 events : list of pandas.DataFrame 

750 

751 confounds : list of pandas.DataFrame or numpy.arrays 

752 

753 run_idx : int 

754 """ 

755 confounds_matrix = None 

756 confounds_names = None 

757 if confounds is not None: 

758 confounds_matrix = confounds[run_idx] 

759 

760 if isinstance(confounds_matrix, pd.DataFrame): 

761 confounds_names = confounds[run_idx].columns.tolist() 

762 confounds_matrix = confounds_matrix.to_numpy() 

763 else: 

764 # create dummy names when dealing with numpy arrays 

765 confounds_names = [ 

766 f"confound_{i}" for i in range(confounds_matrix.shape[1]) 

767 ] 

768 

769 if confounds_matrix.shape[0] != n_scans: 

770 raise ValueError( 

771 "Rows in confounds does not match " 

772 "n_scans in run_img " 

773 f"at index {run_idx}." 

774 ) 

775 

776 tmp = check_and_load_tables(events[run_idx], "events")[0] 

777 if "trial_type" in tmp.columns: 

778 self._reporting_data["trial_types"].extend( 

779 x for x in tmp["trial_type"] if x 

780 ) 

781 

782 start_time = self.slice_time_ref * self.t_r 

783 end_time = (n_scans - 1 + self.slice_time_ref) * self.t_r 

784 frame_times = np.linspace(start_time, end_time, n_scans) 

785 design = make_first_level_design_matrix( 

786 frame_times, 

787 events[run_idx], 

788 self.hrf_model, 

789 self.drift_model, 

790 self.high_pass, 

791 self.drift_order, 

792 self.fir_delays_, 

793 confounds_matrix, 

794 confounds_names, 

795 self.min_onset, 

796 ) 

797 

798 return design 

799 

800 def __sklearn_is_fitted__(self): 

801 return ( 

802 hasattr(self, "labels_") 

803 and hasattr(self, "results_") 

804 and hasattr(self, "fir_delays_") 

805 and self.labels_ is not None 

806 and self.results_ is not None 

807 ) 

808 

809 def fit( 

810 self, 

811 run_imgs, 

812 events=None, 

813 confounds=None, 

814 sample_masks=None, 

815 design_matrices=None, 

816 bins=100, 

817 ): 

818 """Fit the :term:`GLM`. 

819 

820 For each run: 

821 1. create design matrix X 

822 2. do a masker job: fMRI_data -> Y 

823 3. fit regression to (Y, X) 

824 

825 .. warning:: 

826 

827 If design_matrices are passed to fit(), 

828 then the following attributes are ignored: 

829 ``drift_model``, ``drift_order``, ``fir_delays``, ``high_pass``, 

830 ``hrf_model``, ``min_onset``, ``slice_time_ref``, ``t_r``. 

831 

832 Parameters 

833 ---------- 

834 run_imgs : Niimg-like object, \ 

835 :obj:`list` or :obj:`tuple` of Niimg-like objects, \ 

836 SurfaceImage object, \ 

837 or :obj:`list` or \ 

838 :obj:`tuple` of :obj:`~nilearn.surface.SurfaceImage` 

839 Data on which the :term:`GLM` will be fitted. 

840 If this is a list, the affine is considered the same for all. 

841 

842 .. warning:: 

843 

844 If the FirstLevelModel object was instantiated 

845 with a ``mask_img``, 

846 then ``run_imgs`` must be compatible with ``mask_img``. 

847 For example, if ``mask_img`` is 

848 a :class:`nilearn.maskers.NiftiMasker` instance 

849 or a Niimng-like object, then ``run_imgs`` must be a 

850 Niimg-like object, \ 

851 a :obj:`list` or a :obj:`tuple` of Niimg-like objects. 

852 If ``mask_img`` is 

853 a :obj:`~nilearn.maskers.SurfaceMasker` 

854 or :obj:`~nilearn.surface.SurfaceImage` instance, 

855 then ``run_imgs`` must be a 

856 :obj:`~nilearn.surface.SurfaceImage`, \ 

857 a :obj:`list` or \ 

858 a :obj:`tuple` of :obj:`~nilearn.surface.SurfaceImage`. 

859 

860 events : :obj:`pandas.DataFrame` or :obj:`str` or \ 

861 :obj:`pathlib.Path` to a TSV file, or \ 

862 :obj:`list` of \ 

863 :obj:`pandas.DataFrame`, :obj:`str` or \ 

864 :obj:`pathlib.Path` to a TSV file, \ 

865 or None, default=None 

866 :term:`fMRI` events used to build design matrices. 

867 One events object expected per run_img. 

868 Ignored in case designs is not None. 

869 If string, then a path to a csv or tsv file is expected. 

870 See :func:`~nilearn.glm.first_level.make_first_level_design_matrix` 

871 for details on the required content of events files. 

872 

873 .. warning:: 

874 

875 This parameter is ignored if design_matrices are passed. 

876 

877 confounds : :class:`pandas.DataFrame`, :class:`numpy.ndarray` or \ 

878 :obj:`str` or :obj:`list` of :class:`pandas.DataFrame`, \ 

879 :class:`numpy.ndarray` or :obj:`str`, default=None 

880 Each column in a DataFrame corresponds to a confound variable 

881 to be included in the regression model of the respective run_img. 

882 The number of rows must match the number of volumes in the 

883 respective run_img. 

884 Ignored in case designs is not None. 

885 If string, then a path to a csv file is expected. 

886 

887 .. warning:: 

888 

889 This parameter is ignored if design_matrices are passed. 

890 

891 sample_masks : array_like, or :obj:`list` of array_like, default=None 

892 shape of array: (number of scans - number of volumes remove) 

893 Indices of retained volumes. Masks the niimgs along time/fourth 

894 dimension to perform scrubbing (remove volumes with high motion) 

895 and/or remove non-steady-state volumes. 

896 

897 .. versionadded:: 0.9.2 

898 

899 design_matrices : :obj:`pandas.DataFrame` or :obj:`str` or \ 

900 :obj:`pathlib.Path` to a CSV or TSV file, or \ 

901 :obj:`list` of \ 

902 :obj:`pandas.DataFrame`, :obj:`str` or \ 

903 :obj:`pathlib.Path` to a CSV or TSV file, \ 

904 or None, default=None 

905 Design matrices that will be used to fit the GLM. 

906 If given it takes precedence over events and confounds. 

907 

908 bins : :obj:`int`, default=100 

909 Maximum number of discrete bins for the AR coef histogram. 

910 If an autoregressive model with order greater than one is specified 

911 then adaptive quantification is performed and the coefficients 

912 will be clustered via K-means with `bins` number of clusters. 

913 

914 """ 

915 check_params(self.__dict__) 

916 # check attributes passed at construction 

917 if self.t_r is not None: 

918 _check_repetition_time(self.t_r) 

919 

920 if self.slice_time_ref is not None: 

921 _check_slice_time_ref(self.slice_time_ref) 

922 

923 if self.fir_delays is None: 

924 self.fir_delays_ = [0] 

925 else: 

926 self.fir_delays_ = self.fir_delays 

927 

928 self.memory = check_memory(self.memory) 

929 

930 if self.signal_scaling not in {False, 1, (0, 1)}: 

931 raise ValueError( 

932 'signal_scaling must be "False", "0", "1" or "(0, 1)"' 

933 ) 

934 if self.signal_scaling in [0, 1, (0, 1)]: 

935 self.standardize = False 

936 

937 self.labels_ = None 

938 self.results_ = None 

939 

940 run_imgs, events, confounds, sample_masks, design_matrices = ( 

941 self._check_fit_inputs( 

942 run_imgs, 

943 events, 

944 confounds, 

945 sample_masks, 

946 design_matrices, 

947 ) 

948 ) 

949 

950 # Initialize masker_ to None such that attribute exists 

951 self.masker_ = None 

952 

953 self._prepare_mask(run_imgs[0]) 

954 

955 # collect info that may be useful for report generation 

956 drift_model_str = None 

957 if self.drift_model: 

958 if self.drift_model == "cosine": 

959 param_str = f"high pass filter={self.high_pass} Hz" 

960 else: 

961 param_str = f"order={self.drift_order}" 

962 drift_model_str = ( 

963 f"and a {self.drift_model} drift model ({param_str})" 

964 ) 

965 self._reporting_data = { 

966 "trial_types": [], 

967 "noise_model": self.noise_model, 

968 "hrf_model": "finite impulse response" 

969 if self.hrf_model == "fir" 

970 else self.hrf_model, 

971 "drift_model": drift_model_str, 

972 } 

973 

974 self.design_matrices_ = self._create_all_designs( 

975 run_imgs, events, confounds, design_matrices 

976 ) 

977 

978 self._reporting_data["trial_types"] = set( 

979 self._reporting_data["trial_types"] 

980 ) 

981 

982 # For each run fit the model and keep only the regression results. 

983 self.labels_, self.results_ = [], [] 

984 self._reporting_data["run_imgs"] = {} 

985 n_runs = len(run_imgs) 

986 t0 = time.time() 

987 for run_idx, run_img in enumerate(run_imgs): 

988 self._log("progress", run_idx=run_idx, n_runs=n_runs, t0=t0) 

989 

990 # collect name of input files 

991 # for eventual saving to disk later 

992 self._reporting_data["run_imgs"][run_idx] = {} 

993 if isinstance(run_img, (str, Path)): 

994 self._reporting_data["run_imgs"][run_idx] = ( 

995 parse_bids_filename(run_img, legacy=False) 

996 ) 

997 

998 self._fit_single_run(sample_masks, bins, run_img, run_idx) 

999 

1000 self._log("done", n_runs=n_runs, time_in_second=time.time() - t0) 

1001 

1002 return self 

1003 

1004 def compute_contrast( 

1005 self, 

1006 contrast_def, 

1007 stat_type=None, 

1008 output_type="z_score", 

1009 ): 

1010 """Generate different outputs corresponding to \ 

1011 the contrasts provided e.g. z_map, t_map, effects and variance. 

1012 

1013 In multi-run case, outputs the fixed effects map. 

1014 

1015 Parameters 

1016 ---------- 

1017 contrast_def : :obj:`str` \ 

1018 or array of shape (n_col) or \ 

1019 :obj:`list` of (:obj:`str` or array of shape (n_col)) 

1020 

1021 where ``n_col`` is the number of columns of the design matrix, 

1022 (one array per run). If only one array is provided when there 

1023 are several runs, it will be assumed that 

1024 the same :term:`contrast` is 

1025 desired for all runs. One can use the name of the conditions as 

1026 they appear in the design matrix of the fitted model combined with 

1027 operators +- and combined with numbers with operators +-`*`/. In 

1028 this case, the string defining the contrasts must be a valid 

1029 expression for compatibility with :meth:`pandas.DataFrame.eval`. 

1030 

1031 stat_type : {'t', 'F'}, default=None 

1032 Type of the contrast. 

1033 

1034 output_type : :obj:`str`, default='z_score' 

1035 Type of the output map. Can be 'z_score', 'stat', 'p_value', 

1036 :term:`'effect_size'<Parameter Estimate>`, 'effect_variance' or 

1037 'all'. 

1038 

1039 Returns 

1040 ------- 

1041 output : Nifti1Image, :obj:`~nilearn.surface.SurfaceImage`, \ 

1042 or :obj:`dict` 

1043 The desired output image(s). 

1044 If ``output_type == 'all'``, 

1045 then the output is a dictionary of images, 

1046 keyed by the type of image. 

1047 

1048 """ 

1049 check_is_fitted(self) 

1050 

1051 if isinstance(contrast_def, (np.ndarray, str)): 

1052 con_vals = [contrast_def] 

1053 elif isinstance(contrast_def, (list, tuple)): 

1054 con_vals = contrast_def 

1055 else: 

1056 raise ValueError( 

1057 "contrast_def must be an array or str or list of" 

1058 " (array or str)." 

1059 ) 

1060 

1061 n_runs = len(self.labels_) 

1062 n_contrasts = len(con_vals) 

1063 if n_contrasts == 1 and n_runs > 1: 

1064 warn( 

1065 f"One contrast given, assuming it for all {n_runs} runs", 

1066 category=UserWarning, 

1067 stacklevel=find_stack_level(), 

1068 ) 

1069 con_vals = con_vals * n_runs 

1070 elif n_contrasts != n_runs: 

1071 raise ValueError( 

1072 f"{n_contrasts} contrasts given, " 

1073 f"while there are {n_runs} runs." 

1074 ) 

1075 

1076 # Translate formulas to vectors 

1077 for cidx, (con, design_mat) in enumerate( 

1078 zip(con_vals, self.design_matrices_) 

1079 ): 

1080 design_columns = design_mat.columns.tolist() 

1081 if isinstance(con, str): 

1082 con_vals[cidx] = expression_to_contrast_vector( 

1083 con, design_columns 

1084 ) 

1085 

1086 valid_types = [ 

1087 "z_score", 

1088 "stat", 

1089 "p_value", 

1090 "effect_size", 

1091 "effect_variance", 

1092 "all", # must be the final entry! 

1093 ] 

1094 if output_type not in valid_types: 

1095 raise ValueError(f"output_type must be one of {valid_types}") 

1096 contrast = compute_fixed_effect_contrast( 

1097 self.labels_, self.results_, con_vals, stat_type 

1098 ) 

1099 output_types = ( 

1100 valid_types[:-1] if output_type == "all" else [output_type] 

1101 ) 

1102 outputs = {} 

1103 for output_type_ in output_types: 

1104 estimate_ = getattr(contrast, output_type_)() 

1105 # Prepare the returned images 

1106 output = self.masker_.inverse_transform(estimate_) 

1107 contrast_name = str(con_vals) 

1108 if not isinstance(output, SurfaceImage): 

1109 output.header["descrip"] = ( 

1110 f"{output_type_} of contrast {contrast_name}" 

1111 ) 

1112 

1113 outputs[output_type_] = output 

1114 

1115 return outputs if output_type == "all" else output 

1116 

1117 def _get_element_wise_model_attribute( 

1118 self, attribute, result_as_time_series 

1119 ): 

1120 """Transform RegressionResults instances within a dictionary \ 

1121 (whose keys represent the autoregressive coefficient under the 'ar1' \ 

1122 noise model or only 0.0 under 'ols' noise_model and values are the \ 

1123 RegressionResults instances) into an image. 

1124 

1125 Parameters 

1126 ---------- 

1127 attribute : :obj:`str` 

1128 an attribute of a RegressionResults instance. 

1129 possible values include: residuals, normalized_residuals, 

1130 predicted, SSE, r_square, MSE. 

1131 

1132 result_as_time_series : :obj:`bool` 

1133 whether the RegressionResult attribute has a value 

1134 per timepoint of the input nifti image. 

1135 

1136 Returns 

1137 ------- 

1138 output : :obj:`list` 

1139 A list of Nifti1Image(s) or SurfaceImage(s). 

1140 

1141 """ 

1142 # check if valid attribute is being accessed. 

1143 check_is_fitted(self) 

1144 

1145 all_attributes = dict(vars(RegressionResults)).keys() 

1146 possible_attributes = [ 

1147 prop for prop in all_attributes if "__" not in prop 

1148 ] 

1149 if attribute not in possible_attributes: 

1150 msg = f"attribute must be one of: {possible_attributes}" 

1151 raise ValueError(msg) 

1152 

1153 if self.minimize_memory: 

1154 raise ValueError( 

1155 "To access voxelwise attributes like " 

1156 "R-squared, residuals, and predictions, " 

1157 "the `FirstLevelModel`-object needs to store " 

1158 "there attributes. " 

1159 "To do so, set `minimize_memory` to `False` " 

1160 "when initializing the `FirstLevelModel`-object." 

1161 ) 

1162 

1163 output = [] 

1164 

1165 for design_matrix, labels, results in zip( 

1166 self.design_matrices_, self.labels_, self.results_ 

1167 ): 

1168 if result_as_time_series: 

1169 voxelwise_attribute = np.zeros( 

1170 (design_matrix.shape[0], len(labels)) 

1171 ) 

1172 else: 

1173 voxelwise_attribute = np.zeros((1, len(labels))) 

1174 

1175 for label_ in results: 

1176 label_mask = labels == label_ 

1177 voxelwise_attribute[:, label_mask] = getattr( 

1178 results[label_], attribute 

1179 ) 

1180 

1181 output.append(self.masker_.inverse_transform(voxelwise_attribute)) 

1182 

1183 return output 

1184 

1185 def _prepare_mask(self, run_img): 

1186 """Set up the masker. 

1187 

1188 Parameters 

1189 ---------- 

1190 run_img : Niimg-like or :obj:`~nilearn.surface.SurfaceImage` object 

1191 Used for setting up the masker object. 

1192 """ 

1193 # Local import to prevent circular imports 

1194 from nilearn.maskers import NiftiMasker 

1195 

1196 masker_type = "nii" 

1197 # all elements of X should be of the similar type by now 

1198 # so we can only check the first one 

1199 to_check = run_img[0] if isinstance(run_img, Iterable) else run_img 

1200 if not self._is_volume_glm() or isinstance(to_check, SurfaceImage): 

1201 masker_type = "surface" 

1202 

1203 # Learn the mask 

1204 if self.mask_img is False: 

1205 # We create a dummy mask to preserve functionality of api 

1206 if masker_type == "surface": 

1207 surf_data = { 

1208 part: np.ones( 

1209 run_img.data.parts[part].shape[0], dtype=bool 

1210 ) 

1211 for part in run_img.mesh.parts 

1212 } 

1213 self.mask_img = SurfaceImage(mesh=run_img.mesh, data=surf_data) 

1214 else: 

1215 ref_img = check_niimg(run_img) 

1216 self.mask_img = Nifti1Image( 

1217 np.ones(ref_img.shape[:3]), ref_img.affine 

1218 ) 

1219 

1220 if masker_type == "surface" and self.smoothing_fwhm is not None: 

1221 warn( 

1222 "Parameter smoothing_fwhm is not " 

1223 "yet supported for surface data", 

1224 UserWarning, 

1225 stacklevel=find_stack_level(), 

1226 ) 

1227 self.smoothing_fwhm = 0 

1228 

1229 check_compatibility_mask_and_images(self.mask_img, run_img) 

1230 if ( # deal with self.mask_img as image, str, path, none 

1231 (not isinstance(self.mask_img, (NiftiMasker, SurfaceMasker))) 

1232 or 

1233 # edge case: 

1234 # If fitted NiftiMasker with a None mask_img_ attribute 

1235 # the masker parameters are overridden 

1236 # by the FirstLevelModel parameters 

1237 ( 

1238 getattr(self.mask_img, "mask_img_", "not_none") is None 

1239 and self.masker_ is None 

1240 ) 

1241 ): 

1242 self.masker_ = check_embedded_masker( 

1243 self, masker_type, ignore=["high_pass"] 

1244 ) 

1245 

1246 if isinstance(self.masker_, NiftiMasker): 

1247 self.masker_.mask_strategy = "epi" 

1248 

1249 self.masker_.fit(run_img) 

1250 

1251 else: 

1252 check_is_fitted(self.mask_img) 

1253 

1254 self.masker_ = self.mask_img 

1255 

1256 @fill_doc 

1257 def generate_report( 

1258 self, 

1259 contrasts=None, 

1260 title=None, 

1261 bg_img="MNI152TEMPLATE", 

1262 threshold=3.09, 

1263 alpha=0.001, 

1264 cluster_threshold=0, 

1265 height_control="fpr", 

1266 two_sided=False, 

1267 min_distance=8.0, 

1268 plot_type="slice", 

1269 cut_coords=None, 

1270 display_mode=None, 

1271 report_dims=(1600, 800), 

1272 ): 

1273 """Return a :class:`~nilearn.reporting.HTMLReport` \ 

1274 which shows all important aspects of a fitted :term:`GLM`. 

1275 

1276 The :class:`~nilearn.reporting.HTMLReport` can be opened in a 

1277 browser, displayed in a notebook, or saved to disk as a standalone 

1278 HTML file. 

1279 

1280 The :term:`GLM` must be fitted and have the computed design 

1281 matrix(ces). 

1282 

1283 .. note:: 

1284 

1285 Refer to the documentation of 

1286 :func:`~nilearn.reporting.make_glm_report` 

1287 for details about the parameters 

1288 

1289 Returns 

1290 ------- 

1291 report_text : :class:`~nilearn.reporting.HTMLReport` 

1292 Contains the HTML code for the :term:`GLM` report. 

1293 

1294 """ 

1295 from nilearn.reporting.glm_reporter import make_glm_report 

1296 

1297 if not hasattr(self, "_reporting_data"): 

1298 self._reporting_data = { 

1299 "trial_types": [], 

1300 "noise_model": getattr(self, "noise_model", None), 

1301 "hrf_model": getattr(self, "hrf_model", None), 

1302 "drift_model": None, 

1303 } 

1304 

1305 return make_glm_report( 

1306 self, 

1307 contrasts, 

1308 title=title, 

1309 bg_img=bg_img, 

1310 threshold=threshold, 

1311 alpha=alpha, 

1312 cluster_threshold=cluster_threshold, 

1313 height_control=height_control, 

1314 two_sided=two_sided, 

1315 min_distance=min_distance, 

1316 plot_type=plot_type, 

1317 cut_coords=cut_coords, 

1318 display_mode=display_mode, 

1319 report_dims=report_dims, 

1320 ) 

1321 

1322 

1323def _check_events_file_uses_tab_separators(events_files): 

1324 """Raise a ValueError if provided list of text based data files \ 

1325 (.csv, .tsv, etc) do not enforce \ 

1326 the :term:`BIDS` convention of using Tabs as separators. 

1327 

1328 Only scans their first row. 

1329 Does nothing if: 

1330 - If the separator used is :term:`BIDS` compliant. 

1331 - Paths are invalid. 

1332 - File(s) are not text files. 

1333 

1334 Does not flag comma-separated-values-files for compatibility reasons; 

1335 this may change in future as commas are not :term:`BIDS` compliant. 

1336 

1337 Parameters 

1338 ---------- 

1339 events_files : :obj:`str`, List/Tuple[str] 

1340 A single file's path or a collection of filepaths. 

1341 Files are expected to be text files. 

1342 Non-text files will raise ValueError. 

1343 

1344 Returns 

1345 ------- 

1346 None 

1347 

1348 Raises 

1349 ------ 

1350 ValueError: 

1351 If value separators are not Tabs (or commas) 

1352 

1353 """ 

1354 valid_separators = [",", "\t"] 

1355 if not isinstance(events_files, (list, tuple)): 

1356 events_files = [events_files] 

1357 for events_file_ in events_files: 

1358 if isinstance(events_file_, (pd.DataFrame)): 

1359 continue 

1360 try: 

1361 with Path(events_file_).open() as events_file_obj: 

1362 events_file_sample = events_file_obj.readline() 

1363 # The following errors are not being handled here, 

1364 # as they are handled elsewhere in the calling code. 

1365 # Handling them here will break the calling code, 

1366 # and refactoring is not straightforward. 

1367 except OSError: # if invalid filepath. 

1368 pass 

1369 else: 

1370 try: 

1371 csv.Sniffer().sniff( 

1372 sample=events_file_sample, 

1373 delimiters=valid_separators, 

1374 ) 

1375 except csv.Error as e: 

1376 raise ValueError( 

1377 "The values in the events file " 

1378 "are not separated by tabs; " 

1379 "please enforce BIDS conventions", 

1380 events_file_, 

1381 ) from e 

1382 

1383 

1384def _check_run_tables(run_imgs, tables_, tables_name): 

1385 """Check fMRI runs and corresponding tables to raise error if necessary.""" 

1386 _check_length_match(run_imgs, tables_, "run_imgs", tables_name) 

1387 tables_ = check_and_load_tables(tables_, tables_name) 

1388 return tables_ 

1389 

1390 

1391def _check_length_match(list_1, list_2, var_name_1, var_name_2): 

1392 """Check length match of two given inputs to raise error if necessary.""" 

1393 if not isinstance(list_1, list): 

1394 list_1 = [list_1] 

1395 if not isinstance(list_2, list): 

1396 list_2 = [list_2] 

1397 if len(list_1) != len(list_2): 

1398 raise ValueError( 

1399 f"len({var_name_1}) {len(list_1)} does not match " 

1400 f"len({var_name_2}) {len(list_2)}" 

1401 ) 

1402 

1403 

1404def _check_repetition_time(t_r): 

1405 """Check that the repetition time is a positive number.""" 

1406 if not isinstance(t_r, (float, int)): 

1407 raise TypeError( 

1408 f"'t_r' must be a float or an integer. Got {type(t_r)} instead." 

1409 ) 

1410 if t_r <= 0: 

1411 raise ValueError(f"'t_r' must be positive. Got {t_r} instead.") 

1412 

1413 

1414def _check_slice_time_ref(slice_time_ref): 

1415 """Check that slice_time_ref is a number between 0 and 1.""" 

1416 if not isinstance(slice_time_ref, (float, int)): 

1417 raise TypeError( 

1418 "'slice_time_ref' must be a float or an integer. " 

1419 f"Got {type(slice_time_ref)} instead." 

1420 ) 

1421 if slice_time_ref < 0 or slice_time_ref > 1: 

1422 raise ValueError( 

1423 "'slice_time_ref' must be between 0 and 1. " 

1424 f"Got {slice_time_ref} instead." 

1425 ) 

1426 

1427 

1428def first_level_from_bids( 

1429 dataset_path, 

1430 task_label, 

1431 space_label=None, 

1432 sub_labels=None, 

1433 img_filters=None, 

1434 t_r=None, 

1435 slice_time_ref=None, 

1436 hrf_model="glover", 

1437 drift_model="cosine", 

1438 high_pass=0.01, 

1439 drift_order=1, 

1440 fir_delays=None, 

1441 min_onset=-24, 

1442 mask_img=None, 

1443 target_affine=None, 

1444 target_shape=None, 

1445 smoothing_fwhm=None, 

1446 memory=None, 

1447 memory_level=1, 

1448 standardize=False, 

1449 signal_scaling=0, 

1450 noise_model="ar1", 

1451 verbose=0, 

1452 n_jobs=1, 

1453 minimize_memory=True, 

1454 derivatives_folder="derivatives", 

1455 **kwargs, 

1456): 

1457 """Create FirstLevelModel objects and fit arguments \ 

1458 from a :term:`BIDS` dataset. 

1459 

1460 If ``t_r`` is ``None``, this function will attempt 

1461 to load it from a ``bold.json``. 

1462 If ``slice_time_ref`` is ``None``, this function will attempt 

1463 to infer it from a ``bold.json``. 

1464 Otherwise, ``t_r`` and ``slice_time_ref`` are taken as given, 

1465 but a warning may be raised if they are not consistent with the 

1466 ``bold.json``. 

1467 

1468 All parameters not described here are passed to 

1469 :class:`~nilearn.glm.first_level.FirstLevelModel`. 

1470 

1471 The subject label of the model will be determined directly 

1472 from the :term:`BIDS` dataset. 

1473 

1474 Parameters 

1475 ---------- 

1476 dataset_path : :obj:`str` or :obj:`pathlib.Path` 

1477 Directory of the highest level folder of the :term:`BIDS` dataset. 

1478 Should contain subject folders and a derivatives folder. 

1479 

1480 task_label : :obj:`str` 

1481 Task_label as specified in the file names like ``_task-<task_label>_``. 

1482 

1483 space_label : :obj:`str` or None, default=None 

1484 Specifies the space label of the preprocessed bold.nii images. 

1485 As they are specified in the file names like ``_space-<space_label>_``. 

1486 If "fsaverage5" is passed as a value 

1487 then the GLM will be run on pial surface data. 

1488 

1489 sub_labels : :obj:`list` of :obj:`str`, default=None 

1490 Specifies the subset of subject labels to model. 

1491 If ``None``, will model all subjects in the dataset. 

1492 

1493 .. versionadded:: 0.10.1 

1494 

1495 img_filters : :obj:`list` of :obj:`tuple` (:obj:`str`, :obj:`str`), \ 

1496 default=None 

1497 Filters are of the form ``(field, label)``. Only one filter per field 

1498 allowed. 

1499 A file that does not match a filter will be discarded. 

1500 Possible filters are ``'acq'``, ``'ce'``, ``'dir'``, ``'rec'``, 

1501 ``'run'``, ``'echo'``, ``'res'``, ``'den'``, and ``'desc'``. 

1502 Filter examples would be ``('desc', 'preproc')``, ``('dir', 'pa')`` 

1503 and ``('run', '10')``. 

1504 

1505 slice_time_ref : :obj:`float` between ``0.0`` and ``1.0``, or None, \ 

1506 default= None 

1507 This parameter indicates the time of the reference slice used in the 

1508 slice timing preprocessing step of the experimental runs. It is 

1509 expressed as a fraction of the ``t_r`` (time repetition), so it can 

1510 have values between ``0.`` and ``1.`` 

1511 If ``slice_time_ref`` is ``None``, this function will attempt 

1512 to infer it from the metadata found in a ``bold.json``. 

1513 If it cannot be inferred from metadata, it will be set to 0. 

1514 

1515 derivatives_folder : :obj:`str`, default= ``"derivatives"``. 

1516 derivatives and app folder path containing preprocessed files. 

1517 Like ``"derivatives/FMRIPREP"``. 

1518 

1519 kwargs : :obj:`dict` 

1520 

1521 Keyword arguments to be passed to functions called within this 

1522 function. 

1523 

1524 Kwargs prefixed with ``confounds_`` 

1525 will be passed to :func:`~nilearn.interfaces.fmriprep.load_confounds`. 

1526 This allows ``first_level_from_bids`` to return 

1527 a specific set of confounds by relying on confound loading strategies 

1528 defined in :func:`~nilearn.interfaces.fmriprep.load_confounds`. 

1529 If no kwargs are passed, ``first_level_from_bids`` will return 

1530 all the confounds available in the confounds TSV files. 

1531 

1532 .. versionadded:: 0.10.3 

1533 

1534 Examples 

1535 -------- 

1536 If you want to only load 

1537 the rotation and translation motion parameters confounds: 

1538 

1539 .. code-block:: python 

1540 

1541 models, imgs, events, confounds = first_level_from_bids( 

1542 dataset_path=path_to_a_bids_dataset, 

1543 task_label="TaskName", 

1544 space_label="MNI", 

1545 img_filters=[("desc", "preproc")], 

1546 confounds_strategy=("motion"), 

1547 confounds_motion="basic", 

1548 ) 

1549 

1550 If you want to load the motion parameters confounds 

1551 with their derivatives: 

1552 

1553 .. code-block:: python 

1554 

1555 models, imgs, events, confounds = first_level_from_bids( 

1556 dataset_path=path_to_a_bids_dataset, 

1557 task_label="TaskName", 

1558 space_label="MNI", 

1559 img_filters=[("desc", "preproc")], 

1560 confounds_strategy=("motion"), 

1561 confounds_motion="derivatives", 

1562 ) 

1563 

1564 If you additionally want to load 

1565 the confounds with CSF and white matter signal: 

1566 

1567 .. code-block:: python 

1568 

1569 models, imgs, events, confounds = first_level_from_bids( 

1570 dataset_path=path_to_a_bids_dataset, 

1571 task_label="TaskName", 

1572 space_label="MNI", 

1573 img_filters=[("desc", "preproc")], 

1574 confounds_strategy=("motion", "wm_csf"), 

1575 confounds_motion="derivatives", 

1576 confounds_wm_csf="basic", 

1577 ) 

1578 

1579 If you also want to scrub high-motion timepoints: 

1580 

1581 .. code-block:: python 

1582 

1583 models, imgs, events, confounds = first_level_from_bids( 

1584 dataset_path=path_to_a_bids_dataset, 

1585 task_label="TaskName", 

1586 space_label="MNI", 

1587 img_filters=[("desc", "preproc")], 

1588 confounds_strategy=("motion", "wm_csf", "scrub"), 

1589 confounds_motion="derivatives", 

1590 confounds_wm_csf="basic", 

1591 confounds_scrub=1, 

1592 confounds_fd_threshold=0.2, 

1593 confounds_std_dvars_threshold=0, 

1594 ) 

1595 

1596 Please refer to the documentation 

1597 of :func:`~nilearn.interfaces.fmriprep.load_confounds` 

1598 for more details on the confounds loading strategies. 

1599 

1600 Returns 

1601 ------- 

1602 models : list of :class:`~nilearn.glm.first_level.FirstLevelModel` objects 

1603 Each :class:`~nilearn.glm.first_level.FirstLevelModel` object 

1604 corresponds to a subject. 

1605 All runs from different sessions are considered together 

1606 for the same subject to run a fixed effects analysis on them. 

1607 

1608 models_run_imgs : :obj:`list` of list of Niimg-like objects, 

1609 Items for the :class:`~nilearn.glm.first_level.FirstLevelModel` 

1610 fit function of their respective model. 

1611 

1612 models_events : :obj:`list` of list of pandas DataFrames, 

1613 Items for the :class:`~nilearn.glm.first_level.FirstLevelModel` 

1614 fit function of their respective model. 

1615 

1616 models_confounds : :obj:`list` of list of pandas DataFrames or ``None``, 

1617 Items for the :class:`~nilearn.glm.first_level.FirstLevelModel` 

1618 fit function of their respective model. 

1619 

1620 """ 

1621 if memory is None: 

1622 memory = Memory(None) 

1623 if space_label is None: 

1624 space_label = "MNI152NLin2009cAsym" 

1625 

1626 sub_labels = sub_labels or [] 

1627 img_filters = img_filters or [] 

1628 

1629 _check_args_first_level_from_bids( 

1630 dataset_path=dataset_path, 

1631 task_label=task_label, 

1632 space_label=space_label, 

1633 sub_labels=sub_labels, 

1634 img_filters=img_filters, 

1635 derivatives_folder=derivatives_folder, 

1636 ) 

1637 

1638 dataset_path = Path(dataset_path).absolute() 

1639 

1640 kwargs_load_confounds, remaining_kwargs = _check_kwargs_load_confounds( 

1641 **kwargs 

1642 ) 

1643 

1644 if len(remaining_kwargs) > 0: 

1645 raise RuntimeError( 

1646 "Unknown keyword arguments. Keyword arguments should start with " 

1647 f"`confounds_` prefix: {remaining_kwargs}" 

1648 ) 

1649 

1650 if ( 

1651 drift_model is not None 

1652 and kwargs_load_confounds is not None 

1653 and "high_pass" in kwargs_load_confounds.get("strategy") 

1654 ): 

1655 if drift_model == "cosine": 

1656 verb = "duplicate" 

1657 if drift_model == "polynomial": 

1658 verb = "conflict with" 

1659 

1660 warn( 

1661 f"""Confounds will contain a high pass filter, 

1662 that may {verb} the {drift_model} one used in the model. 

1663 Remember to visualize your design matrix before fitting your model 

1664 to check that your model is not overspecified.""", 

1665 UserWarning, 

1666 stacklevel=find_stack_level(), 

1667 ) 

1668 

1669 derivatives_path = Path(dataset_path) / derivatives_folder 

1670 derivatives_path = derivatives_path.absolute() 

1671 

1672 # Get metadata for models. 

1673 # 

1674 # We do it once and assume all subjects and runs 

1675 # have the same value. 

1676 

1677 # Repetition time 

1678 # 

1679 # Try to find a t_r value in the bids datasets 

1680 # If the parameter information is not found in the derivatives folder, 

1681 # a search is done in the raw data folder. 

1682 filters = _make_bids_files_filter( 

1683 task_label=task_label, 

1684 space_label=space_label, 

1685 supported_filters=[ 

1686 *bids_entities()["raw"], 

1687 *bids_entities()["derivatives"], 

1688 ], 

1689 extra_filter=img_filters, 

1690 verbose=verbose, 

1691 ) 

1692 inferred_t_r = infer_repetition_time_from_dataset( 

1693 bids_path=derivatives_path, filters=filters, verbose=verbose 

1694 ) 

1695 if inferred_t_r is None: 

1696 filters = _make_bids_files_filter( 

1697 task_label=task_label, 

1698 supported_filters=[*bids_entities()["raw"]], 

1699 extra_filter=img_filters, 

1700 verbose=verbose, 

1701 ) 

1702 inferred_t_r = infer_repetition_time_from_dataset( 

1703 bids_path=dataset_path, filters=filters, verbose=verbose 

1704 ) 

1705 

1706 if t_r is None and inferred_t_r is not None: 

1707 t_r = inferred_t_r 

1708 if t_r is not None and t_r != inferred_t_r: 

1709 warn( 

1710 f"\n't_r' provided ({t_r}) is different " 

1711 f"from the value found in the BIDS dataset ({inferred_t_r}).\n" 

1712 "Note this may lead to the wrong model specification.", 

1713 stacklevel=find_stack_level(), 

1714 ) 

1715 if t_r is not None: 

1716 _check_repetition_time(t_r) 

1717 else: 

1718 warn( 

1719 "\n't_r' not provided and cannot be inferred from BIDS metadata.\n" 

1720 "It will need to be set manually in the list of models, " 

1721 "otherwise their fit will throw an exception.", 

1722 stacklevel=find_stack_level(), 

1723 ) 

1724 

1725 # Slice time correction reference time 

1726 # 

1727 # Try to infer a slice_time_ref value in the bids derivatives dataset. 

1728 # 

1729 # If no value can be inferred, the default value of 0.0 is used. 

1730 filters = _make_bids_files_filter( 

1731 task_label=task_label, 

1732 space_label=space_label, 

1733 supported_filters=[ 

1734 *bids_entities()["raw"], 

1735 *bids_entities()["derivatives"], 

1736 ], 

1737 extra_filter=img_filters, 

1738 verbose=verbose, 

1739 ) 

1740 StartTime = infer_slice_timing_start_time_from_dataset( 

1741 bids_path=derivatives_path, filters=filters, verbose=verbose 

1742 ) 

1743 if StartTime is not None and t_r is not None: 

1744 assert StartTime < t_r 

1745 inferred_slice_time_ref = StartTime / t_r 

1746 else: 

1747 if slice_time_ref is None: 

1748 warn( 

1749 "'slice_time_ref' not provided " 

1750 "and cannot be inferred from metadata.\n" 

1751 "It will be assumed that the slice timing reference " 

1752 "is 0.0 percent of the repetition time.\n" 

1753 "If it is not the case it will need to " 

1754 "be set manually in the generated list of models.", 

1755 stacklevel=find_stack_level(), 

1756 ) 

1757 inferred_slice_time_ref = 0.0 

1758 

1759 if slice_time_ref is None and inferred_slice_time_ref is not None: 

1760 slice_time_ref = inferred_slice_time_ref 

1761 if ( 

1762 slice_time_ref is not None 

1763 and slice_time_ref != inferred_slice_time_ref 

1764 ): 

1765 warn( 

1766 f"'slice_time_ref' provided ({slice_time_ref}) is different " 

1767 f"from the value found in the BIDS dataset " 

1768 f"({inferred_slice_time_ref}).\n" 

1769 "Note this may lead to the wrong model specification.", 

1770 stacklevel=find_stack_level(), 

1771 ) 

1772 if slice_time_ref is not None: 

1773 _check_slice_time_ref(slice_time_ref) 

1774 

1775 # Build fit_kwargs dictionaries to pass to their respective models fit 

1776 # Events and confounds files must match number of imgs (runs) 

1777 models = [] 

1778 models_run_imgs = [] 

1779 models_events = [] 

1780 models_confounds = [] 

1781 

1782 sub_labels = _list_valid_subjects(derivatives_path, sub_labels) 

1783 if len(sub_labels) == 0: 

1784 raise RuntimeError(f"\nNo subject found in:\n {derivatives_path}") 

1785 for sub_label_ in sub_labels: 

1786 # Create model 

1787 model = FirstLevelModel( 

1788 t_r=t_r, 

1789 slice_time_ref=slice_time_ref, 

1790 hrf_model=hrf_model, 

1791 drift_model=drift_model, 

1792 high_pass=high_pass, 

1793 drift_order=drift_order, 

1794 fir_delays=fir_delays, 

1795 min_onset=min_onset, 

1796 mask_img=mask_img, 

1797 target_affine=target_affine, 

1798 target_shape=target_shape, 

1799 smoothing_fwhm=smoothing_fwhm, 

1800 memory=memory, 

1801 memory_level=memory_level, 

1802 standardize=standardize, 

1803 signal_scaling=signal_scaling, 

1804 noise_model=noise_model, 

1805 verbose=verbose, 

1806 n_jobs=n_jobs, 

1807 minimize_memory=minimize_memory, 

1808 subject_label=sub_label_, 

1809 ) 

1810 models.append(model) 

1811 

1812 imgs, files_to_check = _get_processed_imgs( 

1813 derivatives_path=derivatives_path, 

1814 sub_label=sub_label_, 

1815 task_label=task_label, 

1816 space_label=space_label, 

1817 img_filters=img_filters, 

1818 verbose=verbose, 

1819 ) 

1820 models_run_imgs.append(imgs) 

1821 

1822 events = _get_events_files( 

1823 dataset_path=dataset_path, 

1824 sub_label=sub_label_, 

1825 task_label=task_label, 

1826 img_filters=img_filters, 

1827 imgs=files_to_check, 

1828 verbose=verbose, 

1829 ) 

1830 events = [ 

1831 pd.read_csv(event, sep="\t", index_col=None) for event in events 

1832 ] 

1833 models_events.append(events) 

1834 

1835 confounds = _get_confounds( 

1836 derivatives_path=derivatives_path, 

1837 sub_label=sub_label_, 

1838 task_label=task_label, 

1839 img_filters=img_filters, 

1840 imgs=files_to_check, 

1841 verbose=verbose, 

1842 kwargs_load_confounds=kwargs_load_confounds, 

1843 ) 

1844 models_confounds.append(confounds) 

1845 

1846 return models, models_run_imgs, models_events, models_confounds 

1847 

1848 

1849def _list_valid_subjects(derivatives_path, sub_labels): 

1850 """List valid subjects in the dataset. 

1851 

1852 - Include all subjects if no subject pre-selection is passed. 

1853 - Exclude subjects that do not exist in the derivatives folder. 

1854 - Remove duplicate subjects. 

1855 

1856 Parameters 

1857 ---------- 

1858 derivatives_path : :obj:`str` or :obj:`pathlib.Path` 

1859 Path to the BIDS derivatives folder. 

1860 

1861 sub_labels : :obj:`list` of :obj:`str` 

1862 List of subject labels to process. 

1863 If None, all subjects in the dataset will be processed. 

1864 

1865 Returns 

1866 ------- 

1867 sub_labels : :obj:`list` of :obj:`str` 

1868 List of subject labels that will be processed. 

1869 """ 

1870 derivatives_path = Path(derivatives_path) 

1871 # Infer subjects in dataset if not provided 

1872 if not sub_labels: 

1873 sub_folders = derivatives_path.glob("sub-*/") 

1874 sub_labels = [s.name.split("-")[1] for s in sub_folders if s.is_dir()] 

1875 

1876 # keep only existing subjects 

1877 sub_labels_exist = [] 

1878 for sub_label_ in sub_labels: 

1879 if (derivatives_path / f"sub-{sub_label_}").exists(): 

1880 sub_labels_exist.append(sub_label_) 

1881 else: 

1882 warn( 

1883 f"\nSubject label '{sub_label_}' is not present " 

1884 "in the following dataset and cannot be processed:\n" 

1885 f" {derivatives_path}", 

1886 stacklevel=find_stack_level(), 

1887 ) 

1888 

1889 return sorted(set(sub_labels_exist)) 

1890 

1891 

1892def _report_found_files(files, text, sub_label, filters, verbose): 

1893 """Print list of files found for a given subject and filter. 

1894 

1895 Parameters 

1896 ---------- 

1897 files : :obj:`list` of :obj:`str` 

1898 List of fullpath of files. 

1899 

1900 text : :obj:`str` 

1901 Text description of the file type. 

1902 

1903 sub_label : :obj:`str` 

1904 Subject label as specified in the file names like sub-<sub_label>_. 

1905 

1906 filters : :obj:`list` of :obj:`tuple` (str, str) 

1907 Filters are of the form (field, label). 

1908 Only one filter per field allowed. 

1909 

1910 """ 

1911 unordered_list_string = "\n\t- ".join(files) 

1912 logger.log( 

1913 f"\nFound the following {len(files)} {text} files\n" 

1914 f"- for subject {sub_label}\n" 

1915 f"- for filter: {filters}:\n\t" 

1916 f"- {unordered_list_string}\n", 

1917 verbose=verbose, 

1918 ) 

1919 

1920 

1921def _get_processed_imgs( 

1922 derivatives_path, sub_label, task_label, space_label, img_filters, verbose 

1923): 

1924 """Get images for a given subject, task and filters. 

1925 

1926 Also checks that there is only one images per run / session. 

1927 

1928 Parameters 

1929 ---------- 

1930 derivatives_path : :obj:`str` 

1931 Directory of the derivatives BIDS dataset. 

1932 

1933 sub_label : :obj:`str` 

1934 Subject label as specified in the file names like sub-<sub_label>_. 

1935 

1936 task_label : :obj:`str` 

1937 Task label as specified in the file names like _task-<task_label>_. 

1938 

1939 space_label : None or :obj:`str` 

1940 

1941 img_filters : :obj:`list` of :obj:`tuple` (str, str) 

1942 Filters are of the form (field, label). 

1943 Only one filter per field allowed. 

1944 

1945 verbose : :obj:`integer` 

1946 Indicate the level of verbosity. 

1947 

1948 Returns 

1949 ------- 

1950 imgs : :obj:`list` of :obj:`str`, \ 

1951 or :obj:`list` of :obj:`~nilearn.surface.SurfaceImage` 

1952 List of fullpath to the imgs files 

1953 If fsaverage5 is passed then both hemisphere for each run 

1954 will be loaded into a single SurfaceImage. 

1955 

1956 files_to_check : : :obj:`list` of :obj:`str` 

1957 List of fullpath to imgs files. 

1958 Used for validation 

1959 when finding events or confounds associated with images. 

1960 """ 

1961 filters = _make_bids_files_filter( 

1962 task_label=task_label, 

1963 space_label=space_label, 

1964 supported_filters=bids_entities()["raw"] 

1965 + bids_entities()["derivatives"], 

1966 extra_filter=img_filters, 

1967 verbose=verbose, 

1968 ) 

1969 

1970 if space_label is not None and ( 

1971 space_label == "" or space_label not in ("fsaverage5") 

1972 ): 

1973 imgs = get_bids_files( 

1974 main_path=derivatives_path, 

1975 modality_folder="func", 

1976 file_tag="bold", 

1977 file_type="nii*", 

1978 sub_label=sub_label, 

1979 filters=filters, 

1980 ) 

1981 files_to_report = imgs 

1982 files_to_check = imgs 

1983 

1984 else: 

1985 tmp_filter = filters.copy() 

1986 tmp_filter.append(("hemi", "L")) 

1987 imgs_left = get_bids_files( 

1988 main_path=derivatives_path, 

1989 modality_folder="func", 

1990 file_tag="bold", 

1991 file_type="func.gii", 

1992 sub_label=sub_label, 

1993 filters=tmp_filter, 

1994 ) 

1995 tmp_filter[-1] = ("hemi", "R") 

1996 imgs_right = get_bids_files( 

1997 main_path=derivatives_path, 

1998 modality_folder="func", 

1999 file_tag="bold", 

2000 file_type="func.gii", 

2001 sub_label=sub_label, 

2002 filters=tmp_filter, 

2003 ) 

2004 

2005 # Sanity check to make sure we have the same number of files 

2006 # for each hemisphere 

2007 assert len(imgs_left) == len(imgs_right) 

2008 

2009 imgs = [] 

2010 for data_left, data_right in zip(imgs_left, imgs_right): 

2011 # make sure that filenames only differ by hemisphere 

2012 assert ( 

2013 Path(data_left).stem.replace("hemi-L", "hemi-R") 

2014 == Path(data_right).stem 

2015 ) 

2016 # Assumption: we are loading the data on the pial surface. 

2017 imgs.append( 

2018 SurfaceImage( 

2019 mesh=load_fsaverage()["pial"], 

2020 data={"left": data_left, "right": data_right}, 

2021 ) 

2022 ) 

2023 

2024 files_to_report = imgs_left + imgs_right 

2025 

2026 # Only check the left files 

2027 # as we know they have a right counterpart. 

2028 files_to_check = imgs_left 

2029 

2030 _report_found_files( 

2031 files=files_to_report, 

2032 text="preprocessed BOLD", 

2033 sub_label=sub_label, 

2034 filters=filters, 

2035 verbose=verbose, 

2036 ) 

2037 _check_bids_image_list(files_to_check, sub_label, filters) 

2038 return imgs, files_to_check 

2039 

2040 

2041def _get_events_files( 

2042 dataset_path, 

2043 sub_label, 

2044 task_label, 

2045 img_filters, 

2046 imgs, 

2047 verbose, 

2048): 

2049 """Get events.tsv files for a given subject, task and filters. 

2050 

2051 Also checks that the number of events.tsv files 

2052 matches the number of images. 

2053 

2054 Parameters 

2055 ---------- 

2056 dataset_path : :obj:`str` 

2057 Directory of the derivatives BIDS dataset. 

2058 

2059 sub_label : :obj:`str` 

2060 Subject label as specified in the file names like sub-<sub_label>_. 

2061 

2062 task_label : :obj:`str` 

2063 Task label as specified in the file names like _task-<task_label>_. 

2064 

2065 img_filters : :obj:`list` of :obj:`tuple` (str, str) 

2066 Filters are of the form (field, label). 

2067 Only one filter per field allowed. 

2068 

2069 imgs : :obj:`list` of :obj:`str` 

2070 List of fullpath to the preprocessed images 

2071 

2072 verbose : :obj:`integer` 

2073 Indicate the level of verbosity. 

2074 

2075 Returns 

2076 ------- 

2077 events : :obj:`list` of :obj:`str` 

2078 List of fullpath to the events files 

2079 """ 

2080 # pop the derivatives filter 

2081 # it would otherwise trigger some meaningless warnings 

2082 # as the derivatives entity are not supported in BIDS raw datasets 

2083 img_filters = [ 

2084 x for x in img_filters if x[0] not in bids_entities()["derivatives"] 

2085 ] 

2086 events_filters = _make_bids_files_filter( 

2087 task_label=task_label, 

2088 supported_filters=bids_entities()["raw"], 

2089 extra_filter=img_filters, 

2090 verbose=verbose, 

2091 ) 

2092 events = get_bids_files( 

2093 dataset_path, 

2094 modality_folder="func", 

2095 file_tag="events", 

2096 file_type="tsv", 

2097 sub_label=sub_label, 

2098 filters=events_filters, 

2099 ) 

2100 _report_found_files( 

2101 files=events, 

2102 text="events", 

2103 sub_label=sub_label, 

2104 filters=events_filters, 

2105 verbose=verbose, 

2106 ) 

2107 _check_bids_events_list( 

2108 events=events, 

2109 imgs=imgs, 

2110 sub_label=sub_label, 

2111 task_label=task_label, 

2112 dataset_path=dataset_path, 

2113 events_filters=events_filters, 

2114 verbose=verbose, 

2115 ) 

2116 return events 

2117 

2118 

2119def _get_confounds( 

2120 derivatives_path, 

2121 sub_label, 

2122 task_label, 

2123 img_filters, 

2124 imgs, 

2125 verbose, 

2126 kwargs_load_confounds, 

2127): 

2128 """Get confounds.tsv files for a given subject, task and filters. 

2129 

2130 Also checks that the number of confounds.tsv files 

2131 matches the number of images. 

2132 

2133 Parameters 

2134 ---------- 

2135 derivatives_path : :obj:`str` 

2136 Directory of the derivatives BIDS dataset. 

2137 

2138 sub_label : :obj:`str` 

2139 Subject label as specified in the file names like sub-<sub_label>_. 

2140 

2141 task_label : :obj:`str` 

2142 Task label as specified in the file names like _task-<task_label>_. 

2143 

2144 img_filters : :obj:`list` of :obj:`tuple` (str, str) 

2145 Filters are of the form (field, label). 

2146 Only one filter per field allowed. 

2147 

2148 imgs : :obj:`list` of :obj:`str` 

2149 List of fullpath to the preprocessed images 

2150 

2151 verbose : :obj:`integer` 

2152 Indicate the level of verbosity. 

2153 

2154 Returns 

2155 ------- 

2156 confounds : :obj:`list` of :class:`pandas.DataFrame` 

2157 

2158 """ 

2159 # pop the 'desc' filter 

2160 # it would otherwise trigger some meaningless warnings 

2161 # as desc entity are not supported in BIDS raw datasets 

2162 # and we add a desc-confounds 'filter' later on 

2163 img_filters = [x for x in img_filters if x[0] != "desc"] 

2164 filters = _make_bids_files_filter( 

2165 task_label=task_label, 

2166 supported_filters=bids_entities()["raw"], 

2167 extra_filter=img_filters, 

2168 verbose=verbose, 

2169 ) 

2170 confounds = get_bids_files( 

2171 derivatives_path, 

2172 modality_folder="func", 

2173 file_tag="desc-confounds*", 

2174 file_type="tsv", 

2175 sub_label=sub_label, 

2176 filters=filters, 

2177 ) 

2178 _report_found_files( 

2179 files=confounds, 

2180 text="confounds", 

2181 sub_label=sub_label, 

2182 filters=filters, 

2183 verbose=verbose, 

2184 ) 

2185 _check_confounds_list(confounds=confounds, imgs=imgs) 

2186 

2187 if confounds: 

2188 if kwargs_load_confounds is None: 

2189 confounds = [ 

2190 pd.read_csv(c, sep="\t", index_col=None) for c in confounds 

2191 ] 

2192 return confounds or None 

2193 

2194 confounds, _ = load_confounds(img_files=imgs, **kwargs_load_confounds) 

2195 

2196 return confounds 

2197 

2198 

2199def _check_confounds_list(confounds, imgs): 

2200 """Check the number of confounds.tsv files. 

2201 

2202 If no file is found, it will be assumed there are none, 

2203 but if there are any confounds files, there must be one per run. 

2204 

2205 Parameters 

2206 ---------- 

2207 confounds : :obj:`list` of :obj:`str` 

2208 List of fullpath to the confounds.tsv files 

2209 

2210 imgs : :obj:`list` of :obj:`str` 

2211 List of fullpath to the preprocessed images 

2212 

2213 """ 

2214 if confounds and len(confounds) != len(imgs): 

2215 raise ValueError( 

2216 f"{len(confounds)} confounds.tsv files found " 

2217 f"for {len(imgs)} bold files. " 

2218 "Same number of confound files as " 

2219 "the number of runs is expected" 

2220 ) 

2221 

2222 

2223def _check_args_first_level_from_bids( 

2224 dataset_path, 

2225 task_label, 

2226 space_label, 

2227 sub_labels, 

2228 img_filters, 

2229 derivatives_folder, 

2230): 

2231 """Check type and value of arguments of first_level_from_bids. 

2232 

2233 Check that: 

2234 - dataset_path is a string and exists 

2235 - derivatives_path exists 

2236 - task_label and space_label are valid bids labels 

2237 - img_filters is a list of tuples of strings 

2238 and all filters are valid bids entities 

2239 with valid bids labels 

2240 

2241 Parameters 

2242 ---------- 

2243 dataset_path : :obj:`str` 

2244 Fullpath of the BIDS dataset root folder. 

2245 

2246 task_label : :obj:`str` 

2247 Task_label as specified in the file names like _task-<task_label>_. 

2248 

2249 space_label : :obj:`str` 

2250 Specifies the space label of the preprocessed bold.nii images. 

2251 As they are specified in the file names like _space-<space_label>_. 

2252 

2253 sub_labels : :obj:`list` of :obj:`str`, optional 

2254 Specifies the subset of subject labels to model. 

2255 If 'None', will model all subjects in the dataset. 

2256 

2257 img_filters : :obj:`list` of :obj:`tuples` (str, str) 

2258 Filters are of the form (field, label). 

2259 Only one filter per field allowed. 

2260 

2261 derivatives_path : :obj:`str` 

2262 Fullpath of the BIDS dataset derivative folder. 

2263 

2264 """ 

2265 if not isinstance(dataset_path, (str, Path)): 

2266 raise TypeError( 

2267 "'dataset_path' must be a string or pathlike. " 

2268 f"Got {type(dataset_path)} instead." 

2269 ) 

2270 dataset_path = Path(dataset_path) 

2271 if not dataset_path.exists(): 

2272 raise ValueError(f"'dataset_path' does not exist:\n{dataset_path}") 

2273 

2274 if not isinstance(derivatives_folder, str): 

2275 raise TypeError( 

2276 "'derivatives_folder' must be a string. " 

2277 f"Got {type(derivatives_folder)} instead." 

2278 ) 

2279 derivatives_folder = dataset_path / derivatives_folder 

2280 if not derivatives_folder.exists(): 

2281 raise ValueError( 

2282 "derivatives folder not found in given dataset:\n" 

2283 f"{derivatives_folder}" 

2284 ) 

2285 

2286 check_bids_label(task_label) 

2287 

2288 if space_label is not None: 

2289 check_bids_label(space_label) 

2290 

2291 if not isinstance(sub_labels, list): 

2292 raise TypeError( 

2293 f"sub_labels must be a list, instead {type(sub_labels)} was given" 

2294 ) 

2295 for sub_label_ in sub_labels: 

2296 check_bids_label(sub_label_) 

2297 

2298 if not isinstance(img_filters, list): 

2299 raise TypeError( 

2300 f"'img_filters' must be a list. Got {type(img_filters)} instead." 

2301 ) 

2302 supported_filters = [ 

2303 *bids_entities()["raw"], 

2304 *bids_entities()["derivatives"], 

2305 ] 

2306 for filter_ in img_filters: 

2307 if len(filter_) != 2 or not all(isinstance(x, str) for x in filter_): 

2308 raise TypeError( 

2309 "Filters in img_filters must be (str, str). " 

2310 f"Got {filter_} instead." 

2311 ) 

2312 if filter_[0] not in supported_filters: 

2313 raise ValueError( 

2314 f"Entity {filter_[0]} for {filter_} is not a possible filter. " 

2315 f"Only {supported_filters} are allowed." 

2316 ) 

2317 check_bids_label(filter_[1]) 

2318 

2319 

2320def _check_kwargs_load_confounds(**kwargs): 

2321 # reuse the default from nilearn.interface.fmriprep.load_confounds 

2322 defaults = { 

2323 "strategy": ("motion", "high_pass", "wm_csf"), 

2324 "motion": "full", 

2325 "scrub": 5, 

2326 "fd_threshold": 0.2, 

2327 "std_dvars_threshold": 3, 

2328 "wm_csf": "basic", 

2329 "global_signal": "basic", 

2330 "compcor": "anat_combined", 

2331 "n_compcor": "all", 

2332 "ica_aroma": "full", 

2333 "demean": True, 

2334 } 

2335 

2336 if kwargs.get("confounds_strategy") is None: 

2337 return None, kwargs 

2338 

2339 remaining_kwargs = kwargs.copy() 

2340 kwargs_load_confounds = {} 

2341 for key in defaults: 

2342 confounds_key = f"confounds_{key}" 

2343 if confounds_key in kwargs: 

2344 kwargs_load_confounds[key] = remaining_kwargs.pop(confounds_key) 

2345 else: 

2346 kwargs_load_confounds[key] = defaults[key] 

2347 

2348 return kwargs_load_confounds, remaining_kwargs 

2349 

2350 

2351def _make_bids_files_filter( 

2352 task_label, 

2353 space_label=None, 

2354 supported_filters=None, 

2355 extra_filter=None, 

2356 verbose=0, 

2357): 

2358 """Return a filter to specific files from a BIDS dataset. 

2359 

2360 Parameters 

2361 ---------- 

2362 task_label : :obj:`str` 

2363 Task label as specified in the file names like _task-<task_label>_. 

2364 

2365 space_label : :obj:`str` or None, optional 

2366 Specifies the space label of the preprocessed bold.nii images. 

2367 As they are specified in the file names like _space-<space_label>_. 

2368 

2369 supported_filters : :obj:`list` of :obj:`str` or None, optional 

2370 List of authorized BIDS entities 

2371 

2372 extra_filter : :obj:`list` of :obj:`tuple` (str, str) or None, optional 

2373 Filters are of the form (field, label). 

2374 Only one filter per field allowed. 

2375 

2376 %(verbose0)s 

2377 

2378 Returns 

2379 ------- 

2380 Filter to be used by :func:`get_bids_files`: \ 

2381 :obj:`list` of :obj:`tuple` (str, str) 

2382 filters 

2383 

2384 """ 

2385 filters = [("task", task_label)] 

2386 

2387 if space_label is not None: 

2388 filters.append(("space", space_label)) 

2389 

2390 if extra_filter and supported_filters: 

2391 for filter_ in extra_filter: 

2392 if filter_[0] not in supported_filters: 

2393 if verbose: 

2394 warn( 

2395 f"The filter {filter_} will be skipped. " 

2396 f"'{filter_[0]}' is not among the supported filters. " 

2397 f"Allowed filters include: {supported_filters}", 

2398 stacklevel=find_stack_level(), 

2399 ) 

2400 continue 

2401 

2402 filters.append(filter_) 

2403 

2404 return filters 

2405 

2406 

2407def _check_bids_image_list(imgs, sub_label, filters): 

2408 """Check input BIDS images. 

2409 

2410 Check that: 

2411 - some images were found 

2412 - if more than one image was found, check that there is not more than 

2413 one image for a given session / run combination. 

2414 

2415 Parameters 

2416 ---------- 

2417 imgs : :obj:`list` of :obj:`str` or None 

2418 List of image fullpath filenames. 

2419 

2420 sub_label : :obj:`str` 

2421 Subject label as specified in the file names like _sub-<sub_label>_. 

2422 

2423 filters : :obj:`list` of :obj:`tuple` (str, str) 

2424 Filters of the form (field, label) used to select the files. 

2425 See :func:`get_bids_files`. 

2426 

2427 """ 

2428 if not imgs: 

2429 raise ValueError( 

2430 "No BOLD files found " 

2431 f"for subject {sub_label} " 

2432 f"for filter: {filters}" 

2433 ) 

2434 

2435 if len(imgs) <= 1: 

2436 return 

2437 

2438 msg_start = ( 

2439 "Too many images found\n " 

2440 f"for subject: '{sub_label}'\n" 

2441 f"for filters: {filters}\n" 

2442 ) 

2443 msg_end = ( 

2444 "Please specify it further by setting, " 

2445 "for example, some required task_label, " 

2446 "space_label or img_filters" 

2447 ) 

2448 

2449 run_check_list = [] 

2450 

2451 for img_ in imgs: 

2452 parsed_filename = parse_bids_filename(img_, legacy=False) 

2453 session = parsed_filename["entities"].get("ses") 

2454 run = parsed_filename["entities"].get("run") 

2455 

2456 if session and run: 

2457 if (session, run) in set(run_check_list): 

2458 raise ValueError( 

2459 f"{msg_start}" 

2460 f"for the same run {run} and session {session}. " 

2461 f"{msg_end}" 

2462 ) 

2463 run_check_list.append((session, run)) 

2464 

2465 elif session: 

2466 if session in set(run_check_list): 

2467 raise ValueError( 

2468 f"{msg_start}" 

2469 f"for the same session {session}, " 

2470 "while no additional run specification present. " 

2471 f"{msg_end}" 

2472 ) 

2473 run_check_list.append(session) 

2474 

2475 elif run: 

2476 if run in set(run_check_list): 

2477 raise ValueError( 

2478 f"{msg_start}for the same run {run}. {msg_end}" 

2479 ) 

2480 run_check_list.append(run) 

2481 

2482 

2483def _check_bids_events_list( 

2484 events, imgs, sub_label, task_label, dataset_path, events_filters, verbose 

2485): 

2486 """Check input BIDS events. 

2487 

2488 Check that: 

2489 - some events.tsv files were found 

2490 - as many events.tsv were found as images 

2491 - there is only one events.tsv per image and that they have the same 

2492 raw entities. 

2493 

2494 Parameters 

2495 ---------- 

2496 events : :obj:`list` of :obj:`str` or None 

2497 List of events.tsv fullpath filenames. 

2498 

2499 imgs : :obj:`list` of :obj:`str` 

2500 List of image fullpath filenames. 

2501 

2502 sub_label : :obj:`str` 

2503 Subject label as specified in the file names like sub-<sub_label>_. 

2504 

2505 task_label : :obj:`str` 

2506 Task label as specified in the file names like _task-<task_label>_. 

2507 

2508 dataset_path : :obj:`str` 

2509 Fullpath to the BIDS dataset. 

2510 

2511 events_filters : :obj:`list` of :obj:`tuple` (str, str) 

2512 Filters of the form (field, label) used to select the files. 

2513 See :func:`get_bids_files`. 

2514 

2515 """ 

2516 if not events: 

2517 raise ValueError( 

2518 "No events.tsv files found " 

2519 f"for subject {sub_label} " 

2520 f"for filter: {events_filters}." 

2521 ) 

2522 if len(events) != len(imgs): 

2523 raise ValueError( 

2524 f"{len(events)} events.tsv files found" 

2525 f" for {len(imgs)} bold files. " 

2526 "Same number of event files " 

2527 "as the number of runs is expected." 

2528 ) 

2529 _check_trial_type(events=events) 

2530 

2531 supported_filters = [ 

2532 "sub", 

2533 "ses", 

2534 "task", 

2535 *bids_entities()["raw"], 

2536 ] 

2537 for this_img in imgs: 

2538 parsed_filename = parse_bids_filename(this_img, legacy=False) 

2539 extra_filter = [ 

2540 (entity, parsed_filename["entities"][entity]) 

2541 for entity in parsed_filename["entities"] 

2542 if entity in supported_filters 

2543 ] 

2544 filters = _make_bids_files_filter( 

2545 task_label=task_label, 

2546 space_label=None, 

2547 supported_filters=supported_filters, 

2548 extra_filter=extra_filter, 

2549 verbose=verbose, 

2550 ) 

2551 this_event = get_bids_files( 

2552 dataset_path, 

2553 modality_folder="func", 

2554 file_tag="events", 

2555 file_type="tsv", 

2556 sub_label=sub_label, 

2557 filters=filters, 

2558 ) 

2559 msg_suffix = ( 

2560 f"bold file:\n{this_img}\nfilter:\n{filters})\n" 

2561 "Found all the following events files " 

2562 f"for filter:\n{events}\n" 

2563 ) 

2564 if len(this_event) == 0: 

2565 raise ValueError( 

2566 f"No events.tsv files corresponding to {msg_suffix}" 

2567 ) 

2568 if len(this_event) > 1: 

2569 raise ValueError( 

2570 f"More than 1 events.tsv files corresponding to {msg_suffix}" 

2571 ) 

2572 if this_event[0] not in events: 

2573 raise ValueError( 

2574 f"\n{this_event} not in {events}.\n" 

2575 "No corresponding events.tsv files found " 

2576 f"for {msg_suffix}" 

2577 )