Repository: nilearn/nilearn

Issue:
glm.first_level.hemodynamic_models.compute_regressor() computes wrong TR
https://github.com/nilearn/nilearn/blob/9aa503f599f0835aea2bf9e2720bd5d3e33e941e/nilearn/glm/first_level/hemodynamic_models.py#L569


If your volumes are shifted by half a TR (like after fmriprep preprocessing) and your run is not very long, the TR is getting miscalculated/misestimated here (as is the case for us). Why not e.g. calculate the mean distance between the entries in `frame_times`?

### Nilearn version:
0.9.2.dev

### Expected behavior
return correct regressors

### Actual Behavior
cannot be sure if regressors are correct


Edit: Sorry, just saw the template. This is meant to be a bug report


Discussion:
Hm, this line looks buggy indeed. Your proposition sounds get, but we should probably not take the mean distance, but the min distance (think of irregular acquisitions).
Do you want to open a PR ?
Sure, I'll do that, thanks!

Resolving patch:
diff --git a/nilearn/glm/first_level/hemodynamic_models.py b/nilearn/glm/first_level/hemodynamic_models.py
--- a/nilearn/glm/first_level/hemodynamic_models.py
+++ b/nilearn/glm/first_level/hemodynamic_models.py
@@ -555,9 +555,8 @@ def compute_regressor(exp_condition, hrf_model, frame_times, con_id='cond',
         fir_delays = [int(x) for x in fir_delays]
     oversampling = int(oversampling)
 
-    # this is the average tr in this session, not necessarily the true tr
-    tr = float(frame_times.max()) / (np.size(frame_times) - 1)
-
+    # this is the minimal tr in this session, not necessarily the true tr
+    tr = _calculate_tr(frame_times)
     # 1. create the high temporal resolution regressor
     hr_regressor, hr_frame_times = _sample_condition(
         exp_condition, frame_times, oversampling, min_onset)
@@ -586,3 +585,18 @@ def compute_regressor(exp_condition, hrf_model, frame_times, con_id='cond',
     # 6 generate regressor names
     reg_names = _regressor_names(con_id, hrf_model, fir_delays=fir_delays)
     return computed_regressors, reg_names
+
+
+def _calculate_tr(frame_times):
+    """Calculate TR from differences in frame_times.
+
+    Parameters
+    ----------
+    frame_times : array of shape (n_scans)
+        the desired sampling times
+    Returns
+    -------
+    float
+        repetition time
+    """
+    return np.min(np.diff(frame_times))


Test patch:
diff --git a/nilearn/glm/tests/test_hemodynamic_models.py b/nilearn/glm/tests/test_hemodynamic_models.py
--- a/nilearn/glm/tests/test_hemodynamic_models.py
+++ b/nilearn/glm/tests/test_hemodynamic_models.py
@@ -10,7 +10,7 @@
 
 from nilearn.glm.first_level.hemodynamic_models import \
     (_hrf_kernel, _orthogonalize, _regressor_names, _resample_regressor,
-     _sample_condition, compute_regressor, spm_dispersion_derivative,
+     _sample_condition, compute_regressor, _calculate_tr, spm_dispersion_derivative,
      spm_hrf, spm_time_derivative, glover_dispersion_derivative,
      glover_hrf, glover_time_derivative)
 
@@ -312,6 +312,21 @@ def test_make_regressor_3():
     assert_array_equal(reg, reg_)
 
 
+def test_calculate_tr():
+    """ test the TR calculation
+    """
+    true_tr = 0.75
+    n_vols = 4
+
+    # create times for four volumes, shifted forward by half a TR
+    # (as with fMRIPrep slice timing corrected data)
+    frame_times = np.linspace(
+        true_tr / 2, (n_vols * true_tr) + (true_tr / 2), n_vols + 1)
+
+    estimated_tr = _calculate_tr(frame_times)
+    assert_almost_equal(estimated_tr, true_tr, 2)
+
+
 def test__regressor_names():
     """ test that function allows invalid column identifier
     """