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
« 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.
4Author: Bertrand Thirion, Martin Perez-Guevara, 2016
6"""
8from __future__ import annotations
10import csv
11import inspect
12import time
13from collections.abc import Iterable
14from pathlib import Path
15from warnings import warn
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
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
66def mean_scaling(Y, axis=0):
67 """Scaling of the data to have percent of baseline change \
68 along the specified axis.
70 Parameters
71 ----------
72 Y : array of shape (n_time_points, n_voxels)
73 The input data.
75 axis : :obj:`int`, default=0
76 Axis along which the scaling mean should be calculated.
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.
83 mean : array of shape (n_voxels,)
84 The data mean.
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
101def _ar_model_fit(X, val, Y):
102 """Wrap fit method of ARModel to allow joblib parallelization."""
103 return ARModel(X, val).fit(Y)
106def _yule_walker(x, order):
107 """Compute Yule-Walker (adapted from MNE and statsmodels).
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")
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)
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]
134 rho.shape = x.shape[:-1] + (order,)
135 return rho
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.
144 Parameters
145 ----------
146 Y : array of shape (n_time_points, n_voxels)
147 The :term:`fMRI` data.
149 X : array of shape (n_time_points, n_regressors)
150 The design matrix.
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`.
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.
164 n_jobs : :obj:`int`, default=1
165 The number of CPUs to use to do the computation. -1 means
166 'all CPUs'.
168 %(verbose0)s
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).
174 .. versionadded:: 0.9.1
176 Returns
177 -------
178 labels : array of shape (n_voxels,),
179 A map of values on voxels used to identify the corresponding model.
181 results : :obj:`dict`,
182 Keys correspond to the different labels values
183 values are RegressionResults instances corresponding to the voxels.
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 )
200 # Create the model
201 ols_result = OLSModel(X).fit(Y)
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)
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]
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_]
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_])
241 unique_labels = np.unique(labels)
242 results = {}
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 )
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
257 else:
258 labels = np.zeros(Y.shape[1])
259 results = {0.0: ols_result}
261 return labels, results
264def _check_trial_type(events):
265 """Check that the event files contain a "trial_type" column.
267 Parameters
268 ----------
269 events : :obj:`list` of :obj:`str` or :obj:`pathlib.Path``.
270 A list of paths of events.tsv files.
272 """
273 file_names = []
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)
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 )
293@fill_doc
294class FirstLevelModel(BaseGLM):
295 """Implement the General Linear Model for single run :term:`fMRI` data.
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.
306 .. warning::
308 This parameter is ignored by fit() if design matrices
309 are passed at fit time.
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.
317 .. warning::
319 This parameter is ignored by fit() if design matrices
320 are passed at fit time.
322 %(hrf_model)s
323 Default='glover'.
325 .. warning::
327 This parameter is ignored by fit() if design matrices
328 are passed at fit time.
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.
334 .. warning::
336 This parameter is ignored by fit() if design matrices
337 are passed at fit time.
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'.
343 .. warning::
345 This parameter is ignored by fit() if design matrices
346 are passed at fit time.
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.
352 .. warning::
354 This parameter is ignored by fit() if design matrices
355 are passed at fit time.
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.
363 .. warning::
365 This parameter is ignored by fit() if design matrices
366 are passed at fit time.
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.
373 .. warning::
375 This parameter is ignored by fit() if design matrices
376 are passed at fit time.
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.
390 %(target_affine)s
392 .. note::
393 This parameter is passed to :func:`nilearn.image.resample_img`.
395 %(target_shape)s
397 .. note::
398 This parameter is passed to :func:`nilearn.image.resample_img`.
400 %(smoothing_fwhm)s
402 %(memory)s
404 %(memory_level)s
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.
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).
421 noise_model : {'ar1', 'ols'}, default='ar1'
422 The temporal variance model.
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.
429 %(n_jobs)s
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.
437 subject_label : :obj:`str`, optional
438 This id will be used to identify a `FirstLevelModel` when passed to
439 a `SecondLevelModel` object.
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).
446 .. versionadded:: 0.9.1
448 Attributes
449 ----------
450 labels_ : array of shape (n_voxels,),
451 a map of values on voxels used to identify the corresponding model
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
459 """
461 def __str__(self):
462 return "First Level Model"
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
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
509 self.noise_model = noise_model
510 self.verbose = verbose
511 self.n_jobs = n_jobs
512 self.minimize_memory = minimize_memory
514 # attributes
515 self.subject_label = subject_label
516 self.random_state = random_state
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 )
548 if not isinstance(run_imgs, (list, tuple)):
549 run_imgs = [run_imgs]
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 )
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 )
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 )
597 design_matrices = _check_run_tables(
598 run_imgs, design_matrices, "design_matrices"
599 )
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 )
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")
615 if confounds is not None:
616 confounds = _check_run_tables(run_imgs, confounds, "confounds")
618 if sample_masks is not None:
619 sample_masks = check_run_sample_masks(len(run_imgs), sample_masks)
621 return (
622 run_imgs,
623 events,
624 confounds,
625 sample_masks,
626 design_matrices,
627 )
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 )
649 logger.log(
650 msg,
651 verbose=self.verbose,
652 )
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"
664 return (
665 f"Computing run {run_idx + 1} out of {n_runs} runs ({remaining})."
666 )
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]
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
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)
685 if self.signal_scaling is not False:
686 Y, _ = mean_scaling(Y, self.signal_scaling)
688 if self.memory:
689 mem_glm = self.memory.cache(run_glm, ignore=["n_jobs"])
690 else:
691 mem_glm = run_glm
693 # compute GLM
694 t_glm = time.time()
695 self._log("running")
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 )
706 self._log("run_done", time_in_second=time.time() - t_glm)
708 self.labels_.append(labels)
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
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
725 design_matrices = []
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]
734 design = self._create_single_design(
735 n_scans, events, confounds, run_idx
736 )
738 design_matrices.append(design)
740 return design_matrices
742 def _create_single_design(self, n_scans, events, confounds, run_idx):
743 """Build experimental design of a single run.
745 Parameters
746 ----------
747 n_scans: int
749 events : list of pandas.DataFrame
751 confounds : list of pandas.DataFrame or numpy.arrays
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]
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 ]
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 )
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 )
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 )
798 return design
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 )
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`.
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)
825 .. warning::
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``.
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.
842 .. warning::
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`.
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.
873 .. warning::
875 This parameter is ignored if design_matrices are passed.
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.
887 .. warning::
889 This parameter is ignored if design_matrices are passed.
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.
897 .. versionadded:: 0.9.2
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.
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.
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)
920 if self.slice_time_ref is not None:
921 _check_slice_time_ref(self.slice_time_ref)
923 if self.fir_delays is None:
924 self.fir_delays_ = [0]
925 else:
926 self.fir_delays_ = self.fir_delays
928 self.memory = check_memory(self.memory)
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
937 self.labels_ = None
938 self.results_ = None
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 )
950 # Initialize masker_ to None such that attribute exists
951 self.masker_ = None
953 self._prepare_mask(run_imgs[0])
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 }
974 self.design_matrices_ = self._create_all_designs(
975 run_imgs, events, confounds, design_matrices
976 )
978 self._reporting_data["trial_types"] = set(
979 self._reporting_data["trial_types"]
980 )
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)
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 )
998 self._fit_single_run(sample_masks, bins, run_img, run_idx)
1000 self._log("done", n_runs=n_runs, time_in_second=time.time() - t0)
1002 return self
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.
1013 In multi-run case, outputs the fixed effects map.
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))
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`.
1031 stat_type : {'t', 'F'}, default=None
1032 Type of the contrast.
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'.
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.
1048 """
1049 check_is_fitted(self)
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 )
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 )
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 )
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 )
1113 outputs[output_type_] = output
1115 return outputs if output_type == "all" else output
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.
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.
1132 result_as_time_series : :obj:`bool`
1133 whether the RegressionResult attribute has a value
1134 per timepoint of the input nifti image.
1136 Returns
1137 -------
1138 output : :obj:`list`
1139 A list of Nifti1Image(s) or SurfaceImage(s).
1141 """
1142 # check if valid attribute is being accessed.
1143 check_is_fitted(self)
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)
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 )
1163 output = []
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)))
1175 for label_ in results:
1176 label_mask = labels == label_
1177 voxelwise_attribute[:, label_mask] = getattr(
1178 results[label_], attribute
1179 )
1181 output.append(self.masker_.inverse_transform(voxelwise_attribute))
1183 return output
1185 def _prepare_mask(self, run_img):
1186 """Set up the masker.
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
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"
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 )
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
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 )
1246 if isinstance(self.masker_, NiftiMasker):
1247 self.masker_.mask_strategy = "epi"
1249 self.masker_.fit(run_img)
1251 else:
1252 check_is_fitted(self.mask_img)
1254 self.masker_ = self.mask_img
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`.
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.
1280 The :term:`GLM` must be fitted and have the computed design
1281 matrix(ces).
1283 .. note::
1285 Refer to the documentation of
1286 :func:`~nilearn.reporting.make_glm_report`
1287 for details about the parameters
1289 Returns
1290 -------
1291 report_text : :class:`~nilearn.reporting.HTMLReport`
1292 Contains the HTML code for the :term:`GLM` report.
1294 """
1295 from nilearn.reporting.glm_reporter import make_glm_report
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 }
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 )
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.
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.
1334 Does not flag comma-separated-values-files for compatibility reasons;
1335 this may change in future as commas are not :term:`BIDS` compliant.
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.
1344 Returns
1345 -------
1346 None
1348 Raises
1349 ------
1350 ValueError:
1351 If value separators are not Tabs (or commas)
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
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_
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 )
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.")
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 )
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.
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``.
1468 All parameters not described here are passed to
1469 :class:`~nilearn.glm.first_level.FirstLevelModel`.
1471 The subject label of the model will be determined directly
1472 from the :term:`BIDS` dataset.
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.
1480 task_label : :obj:`str`
1481 Task_label as specified in the file names like ``_task-<task_label>_``.
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.
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.
1493 .. versionadded:: 0.10.1
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')``.
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.
1515 derivatives_folder : :obj:`str`, default= ``"derivatives"``.
1516 derivatives and app folder path containing preprocessed files.
1517 Like ``"derivatives/FMRIPREP"``.
1519 kwargs : :obj:`dict`
1521 Keyword arguments to be passed to functions called within this
1522 function.
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.
1532 .. versionadded:: 0.10.3
1534 Examples
1535 --------
1536 If you want to only load
1537 the rotation and translation motion parameters confounds:
1539 .. code-block:: python
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 )
1550 If you want to load the motion parameters confounds
1551 with their derivatives:
1553 .. code-block:: python
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 )
1564 If you additionally want to load
1565 the confounds with CSF and white matter signal:
1567 .. code-block:: python
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 )
1579 If you also want to scrub high-motion timepoints:
1581 .. code-block:: python
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 )
1596 Please refer to the documentation
1597 of :func:`~nilearn.interfaces.fmriprep.load_confounds`
1598 for more details on the confounds loading strategies.
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.
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.
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.
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.
1620 """
1621 if memory is None:
1622 memory = Memory(None)
1623 if space_label is None:
1624 space_label = "MNI152NLin2009cAsym"
1626 sub_labels = sub_labels or []
1627 img_filters = img_filters or []
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 )
1638 dataset_path = Path(dataset_path).absolute()
1640 kwargs_load_confounds, remaining_kwargs = _check_kwargs_load_confounds(
1641 **kwargs
1642 )
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 )
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"
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 )
1669 derivatives_path = Path(dataset_path) / derivatives_folder
1670 derivatives_path = derivatives_path.absolute()
1672 # Get metadata for models.
1673 #
1674 # We do it once and assume all subjects and runs
1675 # have the same value.
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 )
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 )
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
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)
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 = []
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)
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)
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)
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)
1846 return models, models_run_imgs, models_events, models_confounds
1849def _list_valid_subjects(derivatives_path, sub_labels):
1850 """List valid subjects in the dataset.
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.
1856 Parameters
1857 ----------
1858 derivatives_path : :obj:`str` or :obj:`pathlib.Path`
1859 Path to the BIDS derivatives folder.
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.
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()]
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 )
1889 return sorted(set(sub_labels_exist))
1892def _report_found_files(files, text, sub_label, filters, verbose):
1893 """Print list of files found for a given subject and filter.
1895 Parameters
1896 ----------
1897 files : :obj:`list` of :obj:`str`
1898 List of fullpath of files.
1900 text : :obj:`str`
1901 Text description of the file type.
1903 sub_label : :obj:`str`
1904 Subject label as specified in the file names like sub-<sub_label>_.
1906 filters : :obj:`list` of :obj:`tuple` (str, str)
1907 Filters are of the form (field, label).
1908 Only one filter per field allowed.
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 )
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.
1926 Also checks that there is only one images per run / session.
1928 Parameters
1929 ----------
1930 derivatives_path : :obj:`str`
1931 Directory of the derivatives BIDS dataset.
1933 sub_label : :obj:`str`
1934 Subject label as specified in the file names like sub-<sub_label>_.
1936 task_label : :obj:`str`
1937 Task label as specified in the file names like _task-<task_label>_.
1939 space_label : None or :obj:`str`
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.
1945 verbose : :obj:`integer`
1946 Indicate the level of verbosity.
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.
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 )
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
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 )
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)
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 )
2024 files_to_report = imgs_left + imgs_right
2026 # Only check the left files
2027 # as we know they have a right counterpart.
2028 files_to_check = imgs_left
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
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.
2051 Also checks that the number of events.tsv files
2052 matches the number of images.
2054 Parameters
2055 ----------
2056 dataset_path : :obj:`str`
2057 Directory of the derivatives BIDS dataset.
2059 sub_label : :obj:`str`
2060 Subject label as specified in the file names like sub-<sub_label>_.
2062 task_label : :obj:`str`
2063 Task label as specified in the file names like _task-<task_label>_.
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.
2069 imgs : :obj:`list` of :obj:`str`
2070 List of fullpath to the preprocessed images
2072 verbose : :obj:`integer`
2073 Indicate the level of verbosity.
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
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.
2130 Also checks that the number of confounds.tsv files
2131 matches the number of images.
2133 Parameters
2134 ----------
2135 derivatives_path : :obj:`str`
2136 Directory of the derivatives BIDS dataset.
2138 sub_label : :obj:`str`
2139 Subject label as specified in the file names like sub-<sub_label>_.
2141 task_label : :obj:`str`
2142 Task label as specified in the file names like _task-<task_label>_.
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.
2148 imgs : :obj:`list` of :obj:`str`
2149 List of fullpath to the preprocessed images
2151 verbose : :obj:`integer`
2152 Indicate the level of verbosity.
2154 Returns
2155 -------
2156 confounds : :obj:`list` of :class:`pandas.DataFrame`
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)
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
2194 confounds, _ = load_confounds(img_files=imgs, **kwargs_load_confounds)
2196 return confounds
2199def _check_confounds_list(confounds, imgs):
2200 """Check the number of confounds.tsv files.
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.
2205 Parameters
2206 ----------
2207 confounds : :obj:`list` of :obj:`str`
2208 List of fullpath to the confounds.tsv files
2210 imgs : :obj:`list` of :obj:`str`
2211 List of fullpath to the preprocessed images
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 )
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.
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
2241 Parameters
2242 ----------
2243 dataset_path : :obj:`str`
2244 Fullpath of the BIDS dataset root folder.
2246 task_label : :obj:`str`
2247 Task_label as specified in the file names like _task-<task_label>_.
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>_.
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.
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.
2261 derivatives_path : :obj:`str`
2262 Fullpath of the BIDS dataset derivative folder.
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}")
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 )
2286 check_bids_label(task_label)
2288 if space_label is not None:
2289 check_bids_label(space_label)
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_)
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])
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 }
2336 if kwargs.get("confounds_strategy") is None:
2337 return None, kwargs
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]
2348 return kwargs_load_confounds, remaining_kwargs
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.
2360 Parameters
2361 ----------
2362 task_label : :obj:`str`
2363 Task label as specified in the file names like _task-<task_label>_.
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>_.
2369 supported_filters : :obj:`list` of :obj:`str` or None, optional
2370 List of authorized BIDS entities
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.
2376 %(verbose0)s
2378 Returns
2379 -------
2380 Filter to be used by :func:`get_bids_files`: \
2381 :obj:`list` of :obj:`tuple` (str, str)
2382 filters
2384 """
2385 filters = [("task", task_label)]
2387 if space_label is not None:
2388 filters.append(("space", space_label))
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
2402 filters.append(filter_)
2404 return filters
2407def _check_bids_image_list(imgs, sub_label, filters):
2408 """Check input BIDS images.
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.
2415 Parameters
2416 ----------
2417 imgs : :obj:`list` of :obj:`str` or None
2418 List of image fullpath filenames.
2420 sub_label : :obj:`str`
2421 Subject label as specified in the file names like _sub-<sub_label>_.
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`.
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 )
2435 if len(imgs) <= 1:
2436 return
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 )
2449 run_check_list = []
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")
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))
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)
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)
2483def _check_bids_events_list(
2484 events, imgs, sub_label, task_label, dataset_path, events_filters, verbose
2485):
2486 """Check input BIDS events.
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.
2494 Parameters
2495 ----------
2496 events : :obj:`list` of :obj:`str` or None
2497 List of events.tsv fullpath filenames.
2499 imgs : :obj:`list` of :obj:`str`
2500 List of image fullpath filenames.
2502 sub_label : :obj:`str`
2503 Subject label as specified in the file names like sub-<sub_label>_.
2505 task_label : :obj:`str`
2506 Task label as specified in the file names like _task-<task_label>_.
2508 dataset_path : :obj:`str`
2509 Fullpath to the BIDS dataset.
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`.
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)
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 )