Coverage for nilearn/glm/first_level/hemodynamic_models.py: 17%
152 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"""Hemodynamic response function (hrf) specification.
3Here we provide for SPM, Glover hrfs and finite impulse response (FIR) models.
4This module closely follows SPM implementation
5"""
7import warnings
8from collections.abc import Iterable
10import numpy as np
11from scipy.interpolate import interp1d
12from scipy.linalg import pinv
13from scipy.stats import gamma
15from nilearn._utils import fill_doc, rename_parameters
16from nilearn._utils.logger import find_stack_level
17from nilearn._utils.param_validation import check_params
20def _gamma_difference_hrf(
21 t_r,
22 oversampling=50,
23 time_length=32.0,
24 onset=0.0,
25 delay=6,
26 undershoot=16.0,
27 dispersion=1.0,
28 u_dispersion=1.0,
29 ratio=0.167,
30):
31 """Compute an hrf as the difference of two gamma functions.
33 Parameters
34 ----------
35 t_r : :obj:`float`
36 :term:`Repetition time<TR>`, in seconds (sampling period).
38 oversampling : :obj:`int`, default=50
39 Temporal oversampling factor.
41 time_length : :obj:`float`, default=32
42 hrf kernel length, in seconds.
44 onset : :obj:`float`, default=0
45 Onset time of the hrf.
47 delay : :obj:`float`, default=6
48 Delay parameter of the hrf (in s.).
50 undershoot : :obj:`float`, default=16
51 Undershoot parameter of the hrf (in s.).
53 dispersion : :obj:`float`, default=1
54 Dispersion parameter for the first gamma function.
56 u_dispersion : :obj:`float`, default=1
57 Dispersion parameter for the second gamma function.
59 ratio : :obj:`float`, default=0.167
60 Ratio of the two gamma components.
62 Returns
63 -------
64 hrf : array of shape(length / t_r * oversampling, dtype=float)
65 hrf sampling on the oversampled time grid
67 """
68 dt = t_r / oversampling
69 time_stamps = np.linspace(
70 0, time_length, np.rint(float(time_length) / dt).astype(int)
71 )
72 time_stamps -= onset
74 # define peak and undershoot gamma functions
75 peak_gamma = gamma.pdf(
76 time_stamps, delay / dispersion, loc=dt, scale=dispersion
77 )
78 undershoot_gamma = gamma.pdf(
79 time_stamps, undershoot / u_dispersion, loc=dt, scale=u_dispersion
80 )
82 # calculate the hrf
83 hrf = peak_gamma - ratio * undershoot_gamma
84 hrf /= hrf.sum()
85 return hrf
88@rename_parameters({"tr": "t_r"}, end_version="0.13.0")
89def spm_hrf(t_r, oversampling=50, time_length=32.0, onset=0.0):
90 """Implement the :term:`SPM` :term:`HRF` model.
92 Parameters
93 ----------
94 t_r : :obj:`float`
95 :term:`Repetition time<TR>`, in seconds (sampling period).
97 tr:
99 .. deprecated:: 0.11.0
101 Use ``t_r`` instead (see above).
103 oversampling : :obj:`int`, default=50
104 Temporal oversampling factor.
106 time_length : :obj:`float`, default=32.0
107 :term:`HRF` kernel length, in seconds.
109 onset : :obj:`float`, default=0.0
110 :term:`HRF` onset time, in seconds.
112 Returns
113 -------
114 hrf : array of shape(length / t_r * oversampling, dtype=float)
115 :term:`HRF` sampling on the oversampled time grid
117 """
118 return _gamma_difference_hrf(t_r, oversampling, time_length, onset)
121@rename_parameters({"tr": "t_r"}, end_version="0.13.0")
122def glover_hrf(t_r, oversampling=50, time_length=32.0, onset=0.0):
123 """Implement the Glover :term:`HRF` model.
125 Parameters
126 ----------
127 t_r : :obj:`float`
128 :term:`Repetition time<TR>`, in seconds (sampling period).
130 tr:
132 .. deprecated:: 0.11.0
134 Use ``t_r`` instead (see above).
136 oversampling : :obj:`int`, default=50
137 Temporal oversampling factor.
139 time_length : :obj:`float`, default=32.0
140 :term:`HRF` kernel length, in seconds.
142 onset : :obj:`float`, default=0.0
143 Onset of the response.
145 Returns
146 -------
147 hrf : array of shape(length / t_r * oversampling, dtype=float)
148 :term:`HRF` sampling on the oversampled time grid.
150 """
151 return _gamma_difference_hrf(
152 t_r,
153 oversampling,
154 time_length,
155 onset,
156 delay=6,
157 undershoot=12.0,
158 dispersion=0.9,
159 u_dispersion=0.9,
160 ratio=0.48,
161 )
164def _compute_derivative_from_values(values, values_plus_dt, dt=0.1):
165 """Return the time or dispersion derivative of an hrf."""
166 return 1.0 / dt * (values - values_plus_dt)
169def _generic_time_derivative(
170 func, t_r, oversampling=50, time_length=32.0, onset=0.0, dt=0.1
171):
172 """Return the time derivative of an hrf for a given function.
174 Parameters
175 ----------
176 func : :obj:`function`
177 spm_hrf or glover_hrf
179 t_r : :obj:`float`
180 :term:`Repetition time<TR>`, in seconds (sampling period).
182 oversampling : :obj:`int`, default=50
183 Temporal oversampling factor.
185 time_length : :obj:`float`, default=32
186 hrf kernel length, in seconds.
188 onset : :obj:`float`, default=0
189 Onset of the response.
191 dt : :obj:`float`, default=0.1
192 Time step for the derivative.
193 """
194 return _compute_derivative_from_values(
195 func(t_r, oversampling, time_length, onset),
196 func(t_r, oversampling, time_length, onset + dt),
197 dt=dt,
198 )
201@rename_parameters({"tr": "t_r"}, end_version="0.13.0")
202def spm_time_derivative(t_r, oversampling=50, time_length=32.0, onset=0.0):
203 """Implement the :term:`SPM` time derivative :term:`HRF` (dhrf) model.
205 Parameters
206 ----------
207 t_r : :obj:`float`
208 :term:`Repetition time<TR>`, in seconds (sampling period).
210 tr:
212 .. deprecated:: 0.11.0
214 Use ``t_r`` instead (see above).
216 oversampling : :obj:`int`, default=50
217 Temporal oversampling factor.
219 time_length : :obj:`float`, default=32.0
220 :term:`HRF` kernel length, in seconds.
222 onset : :obj:`float`, default=0.0
223 Onset of the response in seconds.
225 Returns
226 -------
227 dhrf : array of shape(length / t_r, dtype=float)
228 dhrf sampling on the provided grid
230 """
231 return _generic_time_derivative(
232 spm_hrf,
233 t_r=t_r,
234 oversampling=oversampling,
235 time_length=time_length,
236 onset=onset,
237 )
240@rename_parameters({"tr": "t_r"}, end_version="0.13.0")
241def glover_time_derivative(t_r, oversampling=50, time_length=32.0, onset=0.0):
242 """Implement the Glover time derivative :term:`HRF` (dhrf) model.
244 Parameters
245 ----------
246 t_r : :obj:`float`
247 :term:`Repetition time<TR>`, in seconds (sampling period).
249 tr:
251 .. deprecated:: 0.11.0
253 Use ``t_r`` instead (see above).
255 oversampling : :obj:`int`, default=50
256 Temporal oversampling factor.
258 time_length : :obj:`float`, default=32.0
259 :term:`HRF` kernel length, in seconds.
261 onset : :obj:`float`, default=0.0
262 Onset of the response.
264 Returns
265 -------
266 dhrf : array of shape(length / t_r), dtype=float
267 dhrf sampling on the provided grid
269 """
270 return _generic_time_derivative(
271 glover_hrf,
272 t_r=t_r,
273 oversampling=oversampling,
274 time_length=time_length,
275 onset=onset,
276 )
279def _generic_dispersion_derivative(
280 t_r,
281 oversampling=50,
282 time_length=32.0,
283 onset=0.0,
284 undershoot=16,
285 ratio=0.167,
286 dispersion=1.0,
287 dt=0.01,
288):
289 """Return the dispersion derivative of an hrf.
291 Parameters
292 ----------
293 dt : :obj:`float`, default=0.01
294 Dispersion step for the derivative.
296 See _gamma_difference_hrf for the other parameters description.
297 """
298 return _compute_derivative_from_values(
299 _gamma_difference_hrf(
300 t_r,
301 oversampling,
302 time_length,
303 onset,
304 undershoot=undershoot,
305 ratio=ratio,
306 dispersion=dispersion,
307 ),
308 _gamma_difference_hrf(
309 t_r,
310 oversampling,
311 time_length,
312 onset,
313 undershoot=undershoot,
314 ratio=ratio,
315 dispersion=dispersion + dt,
316 ),
317 dt=dt,
318 )
321@rename_parameters({"tr": "t_r"}, end_version="0.13.0")
322def spm_dispersion_derivative(
323 t_r, oversampling=50, time_length=32.0, onset=0.0
324):
325 """Implement the :term:`SPM` dispersion derivative :term:`HRF` model.
327 Parameters
328 ----------
329 t_r : :obj:`float`
330 :term:`Repetition time<TR>`, in seconds (sampling period).
332 tr:
334 .. deprecated:: 0.11.0
336 Use ``t_r`` instead (see above).
338 oversampling : :obj:`int`, default=50
339 Temporal oversampling factor in seconds.
341 time_length : :obj:`float`, default=32.0
342 :term:`HRF` kernel length, in seconds.
344 onset : :obj:`float`, default=0.0
345 Onset of the response in seconds.
347 Returns
348 -------
349 dhrf : array of shape(length / tr * oversampling), dtype=float
350 dhrf sampling on the oversampled time grid
352 """
353 return _generic_dispersion_derivative(
354 t_r, oversampling=oversampling, time_length=time_length, onset=onset
355 )
358@rename_parameters({"tr": "t_r"}, end_version="0.13.0")
359def glover_dispersion_derivative(
360 t_r, oversampling=50, time_length=32.0, onset=0.0
361):
362 """Implement the Glover dispersion derivative :term:`HRF` model.
364 Parameters
365 ----------
366 t_r : :obj:`float`
367 :term:`Repetition time<TR>`, in seconds (sampling period).
369 oversampling : :obj:`int`, default=50
370 Temporal oversampling factor in seconds.
372 tr:
374 .. deprecated:: 0.11.0
376 Use ``t_r`` instead (see above).
378 time_length : :obj:`float`, default=32.0
379 :term:`HRF` kernel length, in seconds.
381 onset : :obj:`float`, default=0.0
382 Onset of the response in seconds.
384 Returns
385 -------
386 dhrf : array of shape(length / t_r * oversampling), dtype=float
387 dhrf sampling on the oversampled time grid
389 """
390 return _generic_dispersion_derivative(
391 t_r,
392 oversampling=oversampling,
393 time_length=time_length,
394 onset=onset,
395 undershoot=12.0,
396 ratio=0.48,
397 dispersion=0.9,
398 )
401def _sample_condition(
402 exp_condition, frame_times, oversampling=50, min_onset=-24
403):
404 """Make a possibly oversampled event regressor from condition information.
406 Parameters
407 ----------
408 exp_condition : arraylike of shape (3, n_events)
409 yields description of events for this condition as a
410 (onsets, durations, amplitudes) triplet
412 frame_times : array of shape(n_scans)
413 Sample time points.
415 oversampling : :obj:`int`, default=50
416 Factor for oversampling event regressor.
418 min_onset : :obj:`float`, default=-24
419 Minimal onset relative to frame_times[0] (in seconds)
420 events that start before frame_times[0] + min_onset are not considered.
422 Returns
423 -------
424 regressor : array of shape(over_sampling * n_scans)
425 Possibly oversampled event regressor.
427 frame_times_high_res : array of shape(over_sampling * n_scans)
428 Time points used for regressor sampling.
430 """
431 # Find the high-resolution frame_times
432 n_frames = frame_times.size
433 min_onset = float(min_onset)
434 n_frames_high_res = _compute_n_frames_high_res(
435 frame_times, min_onset, oversampling
436 )
438 frame_times_high_res = np.linspace(
439 frame_times.min() + min_onset,
440 frame_times.max() * (1 + 1.0 / (n_frames - 1)),
441 np.rint(n_frames_high_res).astype(int),
442 )
444 # Get the condition information
445 onsets, durations, values = tuple(map(np.asanyarray, exp_condition))
446 if (onsets < frame_times[0] + min_onset).any():
447 warnings.warn(
448 (
449 "Some stimulus onsets are earlier "
450 f"than {frame_times[0] + min_onset} in the"
451 " experiment and are thus not considered in the model."
452 ),
453 UserWarning,
454 stacklevel=find_stack_level(),
455 )
457 # Set up the regressor timecourse
458 tmax = len(frame_times_high_res)
459 regressor = np.zeros_like(frame_times_high_res).astype(np.float64)
460 t_onset = np.minimum(
461 np.searchsorted(frame_times_high_res, onsets), tmax - 1
462 )
463 for t, v in zip(t_onset, values):
464 regressor[t] += v
465 t_offset = np.minimum(
466 np.searchsorted(frame_times_high_res, onsets + durations), tmax - 1
467 )
469 # Handle the case where duration is 0 by offsetting at t + 1
470 for i, t in enumerate(t_offset):
471 if t < (tmax - 1) and t == t_onset[i]:
472 t_offset[i] += 1
474 for t, v in zip(t_offset, values):
475 regressor[t] -= v
476 regressor = np.cumsum(regressor)
478 return regressor, frame_times_high_res
481def _compute_n_frames_high_res(frame_times, min_onset, oversampling):
482 """Compute the number of frames after upsampling."""
483 n_frames = frame_times.size
484 mini, maxi = _extrema(frame_times)
485 n_frames_high_res = (n_frames - 1) * 1.0 / (maxi - mini)
486 n_frames_high_res *= (
487 maxi * (1 + 1.0 / (n_frames - 1)) - mini - min_onset
488 ) * oversampling
489 return n_frames_high_res + 1
492def _extrema(arr):
493 """Return the min and max of an array."""
494 return np.min(arr), np.max(arr)
497def _resample_regressor(hr_regressor, frame_times_high_res, frame_times):
498 """Sub-sample the regressors at frame times.
500 Parameters
501 ----------
502 hr_regressor : array of shape(n_samples),
503 the regressor time course sampled at high temporal resolution
505 frame_times_high_res : array of shape(n_samples),
506 the corresponding time stamps
508 frame_times : array of shape(n_scans),
509 the desired time stamps
511 Returns
512 -------
513 regressor : array of shape(n_scans)
514 The resampled regressor.
516 """
517 f = interp1d(frame_times_high_res, hr_regressor)
518 return f(frame_times).T
521def orthogonalize(X):
522 """Orthogonalize every column of design `X` w.r.t preceding columns.
524 Parameters
525 ----------
526 X : array of shape(n, p)
527 The data to be orthogonalized.
529 Returns
530 -------
531 X : array of shape(n, p)
532 The data after orthogonalization.
534 Notes
535 -----
536 X is changed in place. The columns are not normalized.
538 """
539 if X.size == X.shape[0]:
540 return X
542 for i in range(1, X.shape[1]):
543 X[:, i] -= np.dot(np.dot(X[:, i], X[:, :i]), pinv(X[:, :i]))
545 return X
548@fill_doc
549def _regressor_names(con_name, hrf_model, fir_delays=None):
550 """Return a list of regressor names, \
551 computed from con-name and hrf type \
552 when this information is explicitly given.
554 If hrf_model is a custom function or a list of custom functions,
555 return their names.
557 Parameters
558 ----------
559 con_name : :obj:`str`
560 identifier of the condition
561 %(hrf_model)s
562 fir_delays : 1D array_like, optional
563 Delays (in scans) used in case of an FIR model
565 Returns
566 -------
567 names : :obj:`list` of strings,
568 regressor names
570 """
571 check_params(locals())
572 # Default value
573 names = [con_name]
575 # Handle strings
576 if hrf_model in ["glover", "spm"]:
577 names = [con_name]
578 elif hrf_model in ["glover + derivative", "spm + derivative"]:
579 names = [con_name, f"{con_name}_derivative"]
580 elif hrf_model in [
581 "spm + derivative + dispersion",
582 "glover + derivative + dispersion",
583 ]:
584 names = [con_name, f"{con_name}_derivative", f"{con_name}_dispersion"]
585 elif hrf_model == "fir":
586 names = [f"{con_name}_delay_{int(i)}" for i in fir_delays]
587 elif callable(hrf_model):
588 names = [f"{con_name}_{hrf_model.__name__}"]
589 elif isinstance(hrf_model, Iterable) and all(
590 callable(_) for _ in hrf_model
591 ):
592 names = [f"{con_name}_{model.__name__}" for model in hrf_model]
593 elif isinstance(hrf_model, Iterable) and not isinstance(hrf_model, str):
594 names = [f"{con_name}_{i}" for i in range(len(hrf_model))]
596 # Check that all names within the list are different
597 if len(np.unique(names)) != len(names):
598 raise ValueError(f"Computed regressor names are not unique: {names}")
600 return names
603def _hrf_kernel(hrf_model, t_r, oversampling=50, fir_delays=None):
604 """Return the list of matching kernels \
605 given the specification of the hemodynamic model and time parameters.
607 Parameters
608 ----------
609 %(hrf_model)s
611 t_r : :obj:`float`
612 the repetition time in seconds
614 oversampling : :obj:`int`, default=50
615 Temporal oversampling factor to have a smooth hrf.
617 fir_delays : 1D-array-like, optional
618 List of delays (in scans) for finite impulse response models.
620 Returns
621 -------
622 hkernel : :obj:`list` of arrays
623 Samples of the hrf (the number depends on the hrf_model used).
625 """
626 check_params(locals())
627 acceptable_hrfs = [
628 "spm",
629 "spm + derivative",
630 "spm + derivative + dispersion",
631 "fir",
632 "glover",
633 "glover + derivative",
634 "glover + derivative + dispersion",
635 None,
636 ]
637 error_msg = (
638 "Could not process custom HRF model provided. "
639 "Please refer to the related documentation."
640 )
641 if hrf_model == "spm":
642 hkernel = [spm_hrf(t_r, oversampling)]
643 elif hrf_model == "spm + derivative":
644 hkernel = [
645 spm_hrf(t_r, oversampling),
646 spm_time_derivative(t_r, oversampling),
647 ]
648 elif hrf_model == "spm + derivative + dispersion":
649 hkernel = [
650 spm_hrf(t_r, oversampling),
651 spm_time_derivative(t_r, oversampling),
652 spm_dispersion_derivative(t_r, oversampling),
653 ]
654 elif hrf_model == "glover":
655 hkernel = [glover_hrf(t_r, oversampling)]
656 elif hrf_model == "glover + derivative":
657 hkernel = [
658 glover_hrf(t_r, oversampling),
659 glover_time_derivative(t_r, oversampling),
660 ]
661 elif hrf_model == "glover + derivative + dispersion":
662 hkernel = [
663 glover_hrf(t_r, oversampling),
664 glover_time_derivative(t_r, oversampling),
665 glover_dispersion_derivative(t_r, oversampling),
666 ]
667 elif hrf_model == "fir":
668 hkernel = [
669 np.hstack(
670 (
671 np.zeros((f) * oversampling),
672 np.ones(oversampling) * 1.0 / oversampling,
673 )
674 )
675 for f in fir_delays
676 ]
677 elif callable(hrf_model):
678 try:
679 hkernel = [hrf_model(t_r, oversampling)]
680 except TypeError:
681 raise ValueError(error_msg)
682 elif isinstance(hrf_model, Iterable) and all(
683 callable(_) for _ in hrf_model
684 ):
685 try:
686 hkernel = [model(t_r, oversampling) for model in hrf_model]
687 except TypeError:
688 raise ValueError(error_msg)
689 elif hrf_model is None:
690 hkernel = [np.hstack((1, np.zeros(oversampling - 1)))]
691 else:
692 raise ValueError(
693 f'"{hrf_model}" is not a known hrf model. '
694 "Use either a custom model or "
695 f"one of {acceptable_hrfs}"
696 )
697 return hkernel
700@fill_doc
701def compute_regressor(
702 exp_condition,
703 hrf_model,
704 frame_times,
705 con_id="cond",
706 oversampling=50,
707 fir_delays=None,
708 min_onset=-24,
709):
710 """Convolve regressors with :term:`HRF` model.
712 Parameters
713 ----------
714 exp_condition : array-like of shape (3, n_events)
715 yields description of events for this condition as a
716 (onsets, durations, amplitudes) triplet
717 %(hrf_model)s
718 frame_times : array of shape (n_scans)
719 the desired sampling times
721 con_id : :obj:`str`, default='cond'
722 Identifier of the condition
724 oversampling : :obj:`int`, default=50
725 Oversampling factor to perform the convolution.
727 fir_delays : [int] 1D-array-like or None, default=None
728 Delays (in scans) used in case of a finite impulse response model.
730 min_onset : :obj:`float`, default=-24
731 Minimal onset relative to frame_times[0] (in seconds)
732 events that start before frame_times[0] + min_onset are not considered.
734 Returns
735 -------
736 computed_regressors : array of shape(n_scans, n_reg)
737 Computed regressors sampled at frame times.
739 reg_names : :obj:`list` of strings
740 Corresponding regressor names.
742 """
743 check_params(locals())
744 # fir_delays should be integers
745 if fir_delays is not None:
746 fir_delays = [int(x) for x in fir_delays]
747 oversampling = int(oversampling)
749 # this is the minimal t_r in this run, not necessarily the true t_r
750 t_r = _calculate_tr(frame_times)
751 # 1. create the high temporal resolution regressor
752 hr_regressor, frame_times_high_res = _sample_condition(
753 exp_condition, frame_times, oversampling, min_onset
754 )
756 # 2. create the hrf model(s)
757 hkernel = _hrf_kernel(hrf_model, t_r, oversampling, fir_delays)
759 # 3. convolve the regressor and hrf, and downsample the regressor
760 conv_reg = np.array(
761 [np.convolve(hr_regressor, h)[: hr_regressor.size] for h in hkernel]
762 )
764 # 4. temporally resample the regressors
765 if hrf_model == "fir" and oversampling > 1:
766 computed_regressors = _resample_regressor(
767 conv_reg[:, oversampling - 1 :],
768 frame_times_high_res[: 1 - oversampling],
769 frame_times,
770 )
771 else:
772 computed_regressors = _resample_regressor(
773 conv_reg, frame_times_high_res, frame_times
774 )
776 # 5. ortogonalize the regressors
777 if hrf_model != "fir":
778 computed_regressors = orthogonalize(computed_regressors)
780 # 6 generate regressor names
781 reg_names = _regressor_names(con_id, hrf_model, fir_delays=fir_delays)
782 return computed_regressors, reg_names
785def _calculate_tr(frame_times):
786 """Calculate TR from differences in frame_times.
788 Parameters
789 ----------
790 frame_times : array of shape (n_scans)
791 the desired sampling times
793 Returns
794 -------
795 :obj:`float`
796 repetition time
797 """
798 return float(np.min(np.diff(frame_times)))