Skip to content

ENH: Allow BIDS-style slice timings to be passed directly to TShift #2608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 111 additions & 7 deletions nipype/interfaces/afni/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
AFNICommandOutputSpec, AFNIPythonCommandInputSpec,
AFNIPythonCommand, Info, no_afni)

from ...import logging
iflogger = logging.getLogger('nipype.interface')


class CentralityInputSpec(AFNICommandInputSpec):
"""Common input spec class for all centrality-related commands
Expand Down Expand Up @@ -2565,9 +2568,10 @@ class TProject(AFNICommand):
output_spec = AFNICommandOutputSpec



class TShiftInputSpec(AFNICommandInputSpec):
in_file = File(
desc='input file to 3dTShift',
desc='input file to 3dTshift',
argstr='%s',
position=-1,
mandatory=True,
Expand All @@ -2594,12 +2598,26 @@ class TShiftInputSpec(AFNICommandInputSpec):
desc='ignore the first set of points specified', argstr='-ignore %s')
interp = traits.Enum(
('Fourier', 'linear', 'cubic', 'quintic', 'heptic'),
desc='different interpolation methods (see 3dTShift for details) '
desc='different interpolation methods (see 3dTshift for details) '
'default = Fourier',
argstr='-%s')
tpattern = Str(
tpattern = traits.Either(
traits.Enum('alt+z', 'altplus', # Synonyms
'alt+z2',
'alt-z', 'altminus', # Synonyms
'alt-z2',
'seq+z', 'seqplus', # Synonyms
'seq-z', 'seqminus'), # Synonyms
Str, # For backwards compatibility
desc='use specified slice time pattern rather than one in header',
argstr='-tpattern %s')
argstr='-tpattern %s',
xor=['slice_timing'])
slice_timing = traits.Either(
File(exists=True),
traits.List(traits.Float),
desc='time offsets from the volume acquisition onset for each slice',
argstr='-tpattern @%s',
xor=['tpattern'])
rlt = traits.Bool(
desc='Before shifting, remove the mean and linear trend',
argstr='-rlt')
Expand All @@ -2609,6 +2627,10 @@ class TShiftInputSpec(AFNICommandInputSpec):
argstr='-rlt+')


class TShiftOutputSpec(AFNICommandOutputSpec):
timing_file = File(desc="AFNI formatted timing file, if ``slice_timing`` is a list")


class TShift(AFNICommand):
"""Shifts voxel time series from input so that seperate slices are aligned
to the same temporal origin.
Expand All @@ -2619,19 +2641,101 @@ class TShift(AFNICommand):
Examples
========

Slice timing details may be specified explicitly via the ``slice_timing``
input:

>>> from nipype.interfaces import afni
>>> TR = 2.5
>>> tshift = afni.TShift()
>>> tshift.inputs.in_file = 'functional.nii'
>>> tshift.inputs.tzero = 0.0
>>> tshift.inputs.tr = '%.1fs' % TR
>>> tshift.inputs.slice_timing = list(np.arange(40) / TR)
>>> tshift.cmdline
'3dTshift -prefix functional_tshift -tpattern @slice_timing.1D -TR 2.5s -tzero 0.0 functional.nii'

When the ``slice_timing`` input is used, the ``timing_file`` output is populated,
in this case with the generated file.

>>> tshift._list_outputs()['timing_file'] # doctest: +ELLIPSIS
'.../slice_timing.1D'

This method creates a ``slice_timing.1D`` file to be passed to ``3dTshift``.
A pre-existing slice-timing file may be used in the same way:

>>> tshift = afni.TShift()
>>> tshift.inputs.in_file = 'functional.nii'
>>> tshift.inputs.tzero = 0.0
>>> tshift.inputs.tr = '%.1fs' % TR
>>> tshift.inputs.slice_timing = 'slice_timing.1D'
>>> tshift.cmdline
'3dTshift -prefix functional_tshift -tpattern @slice_timing.1D -TR 2.5s -tzero 0.0 functional.nii'

When a pre-existing file is provided, ``timing_file`` is simply passed through.

>>> tshift._list_outputs()['timing_file'] # doctest: +ELLIPSIS
'.../slice_timing.1D'

Alternatively, pre-specified slice timing patterns may be specified with the
``tpattern`` input.
For example, to specify an alternating, ascending slice timing pattern:

>>> tshift = afni.TShift()
>>> tshift.inputs.in_file = 'functional.nii'
>>> tshift.inputs.tzero = 0.0
>>> tshift.inputs.tr = '%.1fs' % TR
>>> tshift.inputs.tpattern = 'alt+z'
>>> tshift.cmdline
'3dTshift -prefix functional_tshift -tpattern alt+z -TR 2.5s -tzero 0.0 functional.nii'

For backwards compatibility, ``tpattern`` may also take filenames prefixed
with ``@``.
However, in this case, filenames are not validated, so this usage will be
deprecated in future versions of Nipype.

>>> tshift = afni.TShift()
>>> tshift.inputs.in_file = 'functional.nii'
>>> tshift.inputs.tzero = 0.0
>>> tshift.inputs.tr = '%.1fs' % TR
>>> tshift.inputs.tpattern = '@slice_timing.1D'
>>> tshift.cmdline
'3dTshift -prefix functional_tshift -tpattern alt+z -tzero 0.0 functional.nii'
>>> res = tshift.run() # doctest: +SKIP
'3dTshift -prefix functional_tshift -tpattern @slice_timing.1D -TR 2.5s -tzero 0.0 functional.nii'

In these cases, ``timing_file`` is undefined.

>>> tshift._list_outputs()['timing_file'] # doctest: +ELLIPSIS
<undefined>

In any configuration, the interface may be run as usual:

>>> res = tshift.run() # doctest: +SKIP
"""
_cmd = '3dTshift'
input_spec = TShiftInputSpec
output_spec = AFNICommandOutputSpec
output_spec = TShiftOutputSpec

def _format_arg(self, name, trait_spec, value):
if name == 'tpattern' and value.startswith('@'):
iflogger.warning('Passing a file prefixed by "@" will be deprecated'
'; please use the `slice_timing` input')
elif name == 'slice_timing' and isinstance(value, list):
value = self._write_slice_timing()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we generate this file, perhaps we should include it in the interface's outputs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable.

return super(TShift, self)._format_arg(name, trait_spec, value)

def _write_slice_timing(self):
fname = 'slice_timing.1D'
with open(fname, 'w') as fobj:
fobj.write('\t'.join(map(str, self.inputs.slice_timing)))
return fname

def _list_outputs(self):
outputs = super(TShift, self)._list_outputs()
if isdefined(self.inputs.slice_timing):
if isinstance(self.inputs.slice_timing, list):
outputs['timing_file'] = os.path.abspath('slice_timing.1D')
else:
outputs['timing_file'] = os.path.abspath(self.inputs.slice_timing)
return outputs


class VolregInputSpec(AFNICommandInputSpec):
Expand Down
14 changes: 12 additions & 2 deletions nipype/interfaces/afni/tests/test_auto_TShift.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ def test_TShift_inputs():
outputtype=dict(),
rlt=dict(argstr='-rlt', ),
rltplus=dict(argstr='-rlt+', ),
tpattern=dict(argstr='-tpattern %s', ),
slice_timing=dict(
argstr='-tpattern @%s',
xor=['tpattern'],
),
tpattern=dict(
argstr='-tpattern %s',
xor=['slice_timing'],
),
tr=dict(argstr='-TR %s', ),
tslice=dict(
argstr='-slice %s',
Expand All @@ -47,7 +54,10 @@ def test_TShift_inputs():
for metakey, value in list(metadata.items()):
assert getattr(inputs.traits()[key], metakey) == value
def test_TShift_outputs():
output_map = dict(out_file=dict(), )
output_map = dict(
out_file=dict(),
timing_file=dict(),
)
outputs = TShift.output_spec()

for key, metadata in list(output_map.items()):
Expand Down
1 change: 1 addition & 0 deletions nipype/testing/data/slice_timing.1D
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0 0.4 0.8 1.2 1.6 2.0 2.4 2.8 3.2 3.6 4.0 4.4 4.8 5.2 5.6 6.0 6.4 6.8 7.2 7.6 8.0 8.4 8.8 9.2 9.6 10.0 10.4 10.8 11.2 11.6 12.0 12.4 12.8 13.2 13.6 14.0 14.4 14.8 15.2 15.6