From cd6b07a9096e888018e330ea7c287d1bbab82e6a Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 5 Jun 2018 09:34:29 -0400 Subject: [PATCH 1/6] ENH: Allow BIDS-style slice timings to be passed directly to TShift --- nipype/interfaces/afni/preprocess.py | 56 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index 19876b1317..b8bb62335b 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('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.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') @@ -2628,11 +2646,41 @@ class TShift(AFNICommand): '3dTshift -prefix functional_tshift -tpattern alt+z -tzero 0.0 functional.nii' >>> res = tshift.run() # doctest: +SKIP + Slice timings may be explicitly specified: + + >>> 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' + + This will create the ``slice_timing.1D`` file in the working directory. + You may wish to remove this after running: + + >>> os.unlink('slice_timing.1D') + """ _cmd = '3dTshift' input_spec = TShiftInputSpec output_spec = AFNICommandOutputSpec + 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 + class VolregInputSpec(AFNICommandInputSpec): in_file = File( From 9448aedaece4d0ecd7896f6e4c4ab0308ad46972 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 5 Jun 2018 09:34:42 -0400 Subject: [PATCH 2/6] TEST: make specs --- nipype/interfaces/afni/tests/test_auto_TShift.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/afni/tests/test_auto_TShift.py b/nipype/interfaces/afni/tests/test_auto_TShift.py index 8f9778b92c..e251d0a4fd 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', From 8aefaf4722da8f2195fb2cb7d5cf26feaa9ce0e7 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 5 Jun 2018 10:56:17 -0400 Subject: [PATCH 3/6] FIX: Use Either(Enum(...), Str) --- nipype/interfaces/afni/preprocess.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index b8bb62335b..a1274fbf25 100644 --- a/nipype/interfaces/afni/preprocess.py +++ b/nipype/interfaces/afni/preprocess.py @@ -2601,13 +2601,13 @@ class TShiftInputSpec(AFNICommandInputSpec): desc='different interpolation methods (see 3dTshift for details) ' 'default = Fourier', argstr='-%s') - tpattern = traits.Enum( - 'alt+z', 'altplus', # Synonyms - 'alt+z2', - 'alt-z', 'altminus', # Synonyms - 'alt-z2', - 'seq+z', 'seqplus', # Synonyms - 'seq-z', 'seqminus', # Synonyms + 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', From a356490d4186bb654f09823950c5132d9bc78367 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 5 Jun 2018 11:13:36 -0400 Subject: [PATCH 4/6] DOC: Improve documentation to delineate all options --- nipype/interfaces/afni/preprocess.py | 45 ++++++++++++++++++++++------ nipype/testing/data/slice_timing.1D | 1 + 2 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 nipype/testing/data/slice_timing.1D diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index a1274fbf25..78cd5262d7 100644 --- a/nipype/interfaces/afni/preprocess.py +++ b/nipype/interfaces/afni/preprocess.py @@ -2637,31 +2637,58 @@ 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.tpattern = 'alt+z' >>> 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 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' - Slice timings may be explicitly specified: + 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: - >>> 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.inputs.slice_timing = 'slice_timing.1D' >>> tshift.cmdline '3dTshift -prefix functional_tshift -tpattern @slice_timing.1D -TR 2.5s -tzero 0.0 functional.nii' - This will create the ``slice_timing.1D`` file in the working directory. - You may wish to remove this after running: + Alternatively, pre-specified slice timing patterns may be specified with the + ``tpattern`` input. + For example, to specify an alternating, ascending slice timing pattern: - >>> os.unlink('slice_timing.1D') + >>> 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 @slice_timing.1D -TR 2.5s -tzero 0.0 functional.nii' + + In any configuration, the interface may be run as usual: + >>> res = tshift.run() # doctest: +SKIP """ _cmd = '3dTshift' input_spec = TShiftInputSpec 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 From 961e47e3507273be14ebbcb26358dc60fbe9889b Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 6 Jul 2018 15:06:33 -0400 Subject: [PATCH 5/6] FIX: nipype.interface logger --- nipype/interfaces/afni/preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index 78cd5262d7..4cec2f233c 100644 --- a/nipype/interfaces/afni/preprocess.py +++ b/nipype/interfaces/afni/preprocess.py @@ -21,7 +21,7 @@ AFNIPythonCommand, Info, no_afni) from ...import logging -iflogger = logging.getLogger('interface') +iflogger = logging.getLogger('nipype.interface') class CentralityInputSpec(AFNICommandInputSpec): From afecc94d6b49b296ff1f8b7e0d98d6f981d260d0 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 23 Jul 2018 16:51:18 -0400 Subject: [PATCH 6/6] ENH: Add timing_file output --- nipype/interfaces/afni/preprocess.py | 31 ++++++++++++++++++- .../interfaces/afni/tests/test_auto_TShift.py | 5 ++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index 4cec2f233c..0658647f2a 100644 --- a/nipype/interfaces/afni/preprocess.py +++ b/nipype/interfaces/afni/preprocess.py @@ -2627,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. @@ -2650,6 +2654,12 @@ class TShift(AFNICommand): >>> 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: @@ -2661,6 +2671,11 @@ class TShift(AFNICommand): >>> 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: @@ -2686,13 +2701,18 @@ class TShift(AFNICommand): >>> tshift.cmdline '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('@'): @@ -2708,6 +2728,15 @@ def _write_slice_timing(self): 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): in_file = File( diff --git a/nipype/interfaces/afni/tests/test_auto_TShift.py b/nipype/interfaces/afni/tests/test_auto_TShift.py index e251d0a4fd..a1208753d9 100644 --- a/nipype/interfaces/afni/tests/test_auto_TShift.py +++ b/nipype/interfaces/afni/tests/test_auto_TShift.py @@ -54,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()):