diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index 19876b1317..0658647f2a 100644 --- a/nipype/interfaces/afni/preprocess.py +++ b/nipype/interfaces/afni/preprocess.py @@ -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 @@ -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, @@ -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') @@ -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. @@ -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 + + + 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() + 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): diff --git a/nipype/interfaces/afni/tests/test_auto_TShift.py b/nipype/interfaces/afni/tests/test_auto_TShift.py index 8f9778b92c..a1208753d9 100644 --- a/nipype/interfaces/afni/tests/test_auto_TShift.py +++ b/nipype/interfaces/afni/tests/test_auto_TShift.py @@ -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', @@ -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()): diff --git a/nipype/testing/data/slice_timing.1D b/nipype/testing/data/slice_timing.1D new file mode 100644 index 0000000000..f240e0d513 --- /dev/null +++ b/nipype/testing/data/slice_timing.1D @@ -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 \ No newline at end of file