Skip to content

Commit 544a874

Browse files
authored
[ENH] add residuals to LSS/LSA interfaces (#294)
* bump nistats version * add residuals to outputs * test existance/shape of residuals * add more comprehensive testing of beta maps
1 parent 66d40b0 commit 544a874

File tree

3 files changed

+93
-6
lines changed

3 files changed

+93
-6
lines changed

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ install_requires =
3131
nipype ~= 1.4.2
3232
pybids ~= 0.9.3
3333
nibabel ~= 2.4.0
34-
nistats == 0.0.1b1
34+
nistats == 0.0.1b2
3535
nilearn ~= 0.4.2
3636
pandas ~= 0.24.0
3737
numpy

src/nibetaseries/interfaces/nistats.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ class LSSBetaSeriesInputSpec(BaseInterfaceInputSpec):
3434
desc="full wide half max smoothing kernel")
3535
high_pass = traits.Float(0.0078125, desc="the high pass filter (Hz)")
3636
fir_delays = traits.Either(None, traits.List(traits.Int),
37-
desc="FIR delays (in scans)")
37+
desc="FIR delays (in scans)",
38+
default=None, usedefault=True)
3839

3940

4041
class LSSBetaSeriesOutputSpec(TraitedSpec):
4142
beta_maps = OutputMultiPath(File)
4243
design_matrices = traits.Dict()
44+
residual = traits.File(exists=True)
4345

4446

4547
class LSSBetaSeries(NistatsBaseInterface, SimpleInterface):
@@ -73,10 +75,12 @@ def _run_interface(self, runtime):
7375
drift_model='cosine',
7476
verbose=1,
7577
fir_delays=self.inputs.fir_delays,
78+
minimize_memory=False,
7679
)
7780

7881
# initialize dictionary to contain trial estimates (betas)
7982
beta_maps = {}
83+
residuals = None
8084
design_matrix_collector = {}
8185
for target_trial_df, trial_type, trial_idx in \
8286
_lss_events_iterator(self.inputs.events_file):
@@ -108,6 +112,21 @@ def _run_interface(self, runtime):
108112
else:
109113
beta_maps[trial_type] = [beta_map]
110114

115+
# add up all the residuals (to be divided later)
116+
if residuals is None:
117+
residuals = model.residuals[0].get_fdata()
118+
else:
119+
residuals += model.residuals[0].get_fdata()
120+
121+
# make an average residual
122+
ave_residual = residuals / (trial_idx + 1)
123+
# make residual nifti image
124+
residual_file = os.path.join(runtime.cwd, 'desc-residuals_bold.nii.gz')
125+
nib.Nifti1Image(
126+
ave_residual,
127+
model.residuals[0].affine,
128+
model.residuals[0].header,
129+
).to_filename(residual_file)
111130
# make a beta series from each beta map list
112131
beta_series_template = os.path.join(runtime.cwd,
113132
'desc-{trial_type}_betaseries.nii.gz')
@@ -120,6 +139,7 @@ def _run_interface(self, runtime):
120139

121140
self._results['beta_maps'] = beta_series_lst
122141
self._results['design_matrices'] = design_matrix_collector
142+
self._results['residual'] = residual_file
123143
return runtime
124144

125145

@@ -147,6 +167,7 @@ class LSABetaSeriesInputSpec(BaseInterfaceInputSpec):
147167
class LSABetaSeriesOutputSpec(TraitedSpec):
148168
beta_maps = OutputMultiPath(File)
149169
design_matrices = traits.List()
170+
residual = traits.File(exists=True)
150171

151172

152173
class LSABetaSeries(NistatsBaseInterface, SimpleInterface):
@@ -178,7 +199,8 @@ def _run_interface(self, runtime):
178199
signal_scaling=0,
179200
high_pass=self.inputs.high_pass,
180201
drift_model='cosine',
181-
verbose=1
202+
verbose=1,
203+
minimize_memory=False,
182204
)
183205

184206
# initialize dictionary to contain trial estimates (betas)
@@ -199,6 +221,9 @@ def _run_interface(self, runtime):
199221
else:
200222
beta_maps[t_type] = [beta_map]
201223

224+
# calculate the residual
225+
residual_file = os.path.join(runtime.cwd, 'desc-residuals_bold.nii.gz')
226+
model.residuals[0].to_filename(residual_file)
202227
# make a beta series from each beta map list
203228
beta_series_template = os.path.join(runtime.cwd,
204229
'desc-{trial_type}_betaseries.nii.gz')
@@ -211,6 +236,7 @@ def _run_interface(self, runtime):
211236

212237
self._results['beta_maps'] = beta_series_lst
213238
self._results['design_matrices'] = [design_matrix]
239+
self._results['residual'] = residual_file
214240
return runtime
215241

216242

src/nibetaseries/interfaces/tests/test_nistats.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
''' Testing module for nibetaseries.interfaces.nistats '''
22
import os
33
import json
4+
import re
45

6+
import nibabel as nib
7+
from nilearn.image import load_img
8+
import pandas as pd
59
import pytest
610

711

@@ -16,7 +20,6 @@ def test_lss_beta_series(sub_metadata, preproc_file, sub_events,
1620
"""Test lss interface with nibabel nifti images
1721
"""
1822
if use_nibabel:
19-
import nibabel as nib
2023
bold_file = nib.load(str(preproc_file))
2124
mask_file = nib.load(str(brainmask_file))
2225
else:
@@ -39,16 +42,35 @@ def test_lss_beta_series(sub_metadata, preproc_file, sub_events,
3942
high_pass=0.008)
4043
res = beta_series.run()
4144

45+
events_df = pd.read_csv(str(sub_events), sep='\t')
46+
trial_types = events_df['trial_type'].unique()
47+
48+
# check for the correct number of beta maps
49+
assert len(trial_types) == len(res.outputs.beta_maps)
50+
51+
input_img_dim = load_img(bold_file).shape[:3]
4252
for beta_map in res.outputs.beta_maps:
53+
trial_type = re.search(r'desc-([A-Za-z0-9]+)_', beta_map).groups()[0]
54+
# check if correct trial type is made
55+
assert trial_type in trial_types
56+
# check if output is actually a file
4357
assert os.path.isfile(beta_map)
58+
# check if image dimensions are correct
59+
assert input_img_dim == load_img(beta_map).shape[:3]
60+
# check if number of trials are correct
61+
assert (events_df['trial_type'] == trial_type).sum() == load_img(beta_map).shape[-1]
62+
# clean up
4463
os.remove(beta_map)
4564

65+
# check residual image
66+
assert load_img(bold_file).shape == load_img(res.outputs.residual).shape
67+
os.remove(res.outputs.residual)
68+
4669

4770
@pytest.mark.parametrize("use_nibabel", [(True), (False)])
4871
def test_fs_beta_series(sub_metadata, preproc_file, sub_events,
4972
confounds_file, brainmask_file, use_nibabel):
5073
if use_nibabel:
51-
import nibabel as nib
5274
bold_file = nib.load(str(preproc_file))
5375
mask_file = nib.load(str(brainmask_file))
5476
else:
@@ -73,16 +95,35 @@ def test_fs_beta_series(sub_metadata, preproc_file, sub_events,
7395
high_pass=0.008)
7496
res = beta_series.run()
7597

98+
events_df = pd.read_csv(str(sub_events), sep='\t')
99+
trial_types = events_df['trial_type'].unique()
100+
101+
# check for the correct number of beta maps
102+
assert len(trial_types) * len(fir_delays) == len(res.outputs.beta_maps)
103+
104+
input_img_dim = load_img(bold_file).shape[:3]
76105
for beta_map in res.outputs.beta_maps:
106+
trial_type = re.search(r'desc-([A-Za-z0-9]+)Delay[0-9]+Vol_', beta_map).groups()[0]
107+
# check if correct trial type is made
108+
assert trial_type in trial_types
109+
# check if output is actually a file
77110
assert os.path.isfile(beta_map)
111+
# check if image dimensions are correct
112+
assert input_img_dim == load_img(beta_map).shape[:3]
113+
# check if number of trials are correct
114+
assert (events_df['trial_type'] == trial_type).sum() == load_img(beta_map).shape[-1]
115+
# clean up
78116
os.remove(beta_map)
79117

118+
# check residual image
119+
assert load_img(bold_file).shape == load_img(res.outputs.residual).shape
120+
os.remove(res.outputs.residual)
121+
80122

81123
@pytest.mark.parametrize("use_nibabel", [(True), (False)])
82124
def test_lsa_beta_series(sub_metadata, preproc_file, sub_events,
83125
confounds_file, brainmask_file, use_nibabel):
84126
if use_nibabel:
85-
import nibabel as nib
86127
bold_file = nib.load(str(preproc_file))
87128
mask_file = nib.load(str(brainmask_file))
88129
else:
@@ -105,10 +146,30 @@ def test_lsa_beta_series(sub_metadata, preproc_file, sub_events,
105146
high_pass=0.008)
106147
res = beta_series.run()
107148

149+
events_df = pd.read_csv(str(sub_events), sep='\t')
150+
trial_types = events_df['trial_type'].unique()
151+
152+
# check for the correct number of beta maps
153+
assert len(trial_types) == len(res.outputs.beta_maps)
154+
155+
input_img_dim = load_img(bold_file).shape[:3]
108156
for beta_map in res.outputs.beta_maps:
157+
trial_type = re.search(r'desc-([A-Za-z0-9]+)_', beta_map).groups()[0]
158+
# check if correct trial type is made
159+
assert trial_type in trial_types
160+
# check if output is actually a file
109161
assert os.path.isfile(beta_map)
162+
# check if image dimensions are correct
163+
assert input_img_dim == load_img(beta_map).shape[:3]
164+
# check if number of trials are correct
165+
assert (events_df['trial_type'] == trial_type).sum() == load_img(beta_map).shape[-1]
166+
# clean up
110167
os.remove(beta_map)
111168

169+
# check residual image
170+
assert load_img(bold_file).shape == load_img(res.outputs.residual).shape
171+
os.remove(res.outputs.residual)
172+
112173

113174
def test_lss_events_iterator(sub_events):
114175
# all but the first instance of waffle

0 commit comments

Comments
 (0)