Repository: nipy/nipype

Issue:
BUG: Cannot use translation_threshold in ArtifactDetect
### Summary

ArtifactDetect currently doesn't allow `translation_threshold` to be used, because it always thinks `norm_threshold` is set.

### Actual behavior

Error is thrown: "ArtifactDetect requires a value for input 'use_norm' because one of norm_threshold is set. For a list of required inputs, see ArtifactDetect.help()"

### Expected behavior

ArtifactDetect should use `translation_threshold` or (in the reduced example below) it should throw an error because the files are empty.

### How to replicate the behavior

Create an empty file called "empty.file" (`touch empty.file`), then run the following file (python2 will probably give the same result).

### Script/Workflow details

```
#!/usr/bin/env python3

import nipype.algorithms.rapidart as ra

art = ra.ArtifactDetect(
    mask_type='file',
    translation_threshold=1.0,  # translation in mm
    rotation_threshold=0.02,  # rotation in radians
    zintensity_threshold=3.0,  # z-score
    use_norm=False,
    parameter_source='AFNI',
    save_plot=True,
    realignment_parameters='empty.file',
    realigned_files='empty.file',
    mask_file='empty.file',
)
art.run()
```

### Platform details:

Please paste the output of: `python -c "import nipype; print(nipype.get_info()); print(nipype.__version__)"`

> {'traits_version': '4.6.0', 'commit_hash': 'ab14a0e', 'pkg_path': '/home/jonathan/.virtualenvs/mri-py3/lib/python3.4/site-packages/nipype', 'nibabel_version': '2.1.0', 'sys_version': '3.4.2 (default, Oct  8 2014, 10:45:20) \n[GCC 4.9.1]', 'nipype_version': '1.0.0-dev', 'scipy_version': '0.19.1', 'commit_source': 'installation', 'sys_executable': '/home/jonathan/.virtualenvs/mri-py3/bin/python', 'numpy_version': '1.13.1', 'networkx_version': '1.11', 'sys_platform': 'linux'}
> 1.0.0-dev

### Execution environment

- My python environment outside container

I'm running the code in a virtualenv.



Discussion:

Resolving patch:
diff --git a/nipype/algorithms/rapidart.py b/nipype/algorithms/rapidart.py
--- a/nipype/algorithms/rapidart.py
+++ b/nipype/algorithms/rapidart.py
@@ -18,7 +18,8 @@
    >>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data'))
    >>> os.chdir(datadir)
 """
-from __future__ import print_function, division, unicode_literals, absolute_import
+from __future__ import (print_function, division,
+                        unicode_literals, absolute_import)
 from builtins import open, range, str, bytes
 
 import os
@@ -160,45 +161,61 @@ def _calc_norm_affine(affines, use_differences, brain_pts=None):
 
 class ArtifactDetectInputSpec(BaseInterfaceInputSpec):
     realigned_files = InputMultiPath(File(exists=True),
-                                     desc="Names of realigned functional data files",
+                                     desc=("Names of realigned functional data "
+                                           "files"),
                                      mandatory=True)
-    realignment_parameters = InputMultiPath(File(exists=True), mandatory=True,
-                                            desc=("Names of realignment parameters"
-                                                  "corresponding to the functional data files"))
+    realignment_parameters = InputMultiPath(File(exists=True),
+                                            mandatory=True,
+                                            desc=("Names of realignment "
+                                                  "parameters corresponding to "
+                                                  "the functional data files"))
     parameter_source = traits.Enum("SPM", "FSL", "AFNI", "NiPy", "FSFAST",
                                    desc="Source of movement parameters",
                                    mandatory=True)
-    use_differences = traits.ListBool([True, False], minlen=2, maxlen=2,
+    use_differences = traits.ListBool([True, False],
+                                      minlen=2,
+                                      maxlen=2,
                                       usedefault=True,
-                                      desc=("Use differences between successive motion (first element)"
-                                            "and intensity paramter (second element) estimates in order"
-                                            "to determine outliers.  (default is [True, False])"))
-    use_norm = traits.Bool(True, requires=['norm_threshold'],
+                                      desc=("Use differences between successive"
+                                            " motion (first element) and "
+                                            "intensity parameter (second "
+                                            "element) estimates in order to "
+                                            "determine outliers.  "
+                                            "(default is [True, False])"))
+    use_norm = traits.Bool(True,
+                           usedefault=True,
+                           requires=['norm_threshold'],
                            desc=("Uses a composite of the motion parameters in "
-                                 "order to determine outliers."),
-                           usedefault=True)
-    norm_threshold = traits.Float(desc=("Threshold to use to detect motion-rela"
+                                 "order to determine outliers."))
+    norm_threshold = traits.Float(xor=['rotation_threshold',
+                                       'translation_threshold'],
+                                  mandatory=True,
+                                  desc=("Threshold to use to detect motion-rela"
                                         "ted outliers when composite motion is "
-                                        "being used"), mandatory=True,
-                                  xor=['rotation_threshold',
-                                       'translation_threshold'])
-    rotation_threshold = traits.Float(mandatory=True, xor=['norm_threshold'],
-                                      desc=("Threshold (in radians) to use to detect rotation-related "
-                                            "outliers"))
-    translation_threshold = traits.Float(mandatory=True, xor=['norm_threshold'],
-                                         desc=("Threshold (in mm) to use to detect translation-related "
+                                        "being used"))
+    rotation_threshold = traits.Float(mandatory=True,
+                                      xor=['norm_threshold'],
+                                      desc=("Threshold (in radians) to use to "
+                                            "detect rotation-related outliers"))
+    translation_threshold = traits.Float(mandatory=True,
+                                         xor=['norm_threshold'],
+                                         desc=("Threshold (in mm) to use to "
+                                               "detect translation-related "
                                                "outliers"))
     zintensity_threshold = traits.Float(mandatory=True,
-                                        desc=("Intensity Z-threshold use to detection images that deviate "
+                                        desc=("Intensity Z-threshold use to "
+                                              "detection images that deviate "
                                               "from the mean"))
     mask_type = traits.Enum('spm_global', 'file', 'thresh',
-                            desc=("Type of mask that should be used to mask the functional "
-                                  "data. *spm_global* uses an spm_global like calculation to "
-                                  "determine the brain mask. *file* specifies a brain mask "
-                                  "file (should be an image file consisting of 0s and 1s). "
-                                  "*thresh* specifies a threshold to use. By default all voxels"
-                                  "are used, unless one of these mask types are defined."),
-                            mandatory=True)
+                            mandatory=True,
+                            desc=("Type of mask that should be used to mask the"
+                                  " functional data. *spm_global* uses an "
+                                  "spm_global like calculation to determine the"
+                                  " brain mask. *file* specifies a brain mask "
+                                  "file (should be an image file consisting of "
+                                  "0s and 1s). *thresh* specifies a threshold "
+                                  "to use. By default all voxels are used,"
+                                  "unless one of these mask types are defined"))
     mask_file = File(exists=True,
                      desc="Mask file to be used if mask_type is 'file'.")
     mask_threshold = traits.Float(desc=("Mask threshold to be used if mask_type"
@@ -224,28 +241,36 @@ class ArtifactDetectInputSpec(BaseInterfaceInputSpec):
 
 class ArtifactDetectOutputSpec(TraitedSpec):
     outlier_files = OutputMultiPath(File(exists=True),
-                                    desc=("One file for each functional run containing a list of "
-                                          "0-based indices corresponding to outlier volumes"))
+                                    desc=("One file for each functional run "
+                                          "containing a list of 0-based indices"
+                                          " corresponding to outlier volumes"))
     intensity_files = OutputMultiPath(File(exists=True),
-                                      desc=("One file for each functional run containing the global "
-                                            "intensity values determined from the brainmask"))
+                                      desc=("One file for each functional run "
+                                            "containing the global intensity "
+                                            "values determined from the "
+                                            "brainmask"))
     norm_files = OutputMultiPath(File,
-                                 desc=("One file for each functional run containing the composite "
-                                       "norm"))
+                                 desc=("One file for each functional run "
+                                       "containing the composite norm"))
     statistic_files = OutputMultiPath(File(exists=True),
-                                      desc=("One file for each functional run containing information "
-                                            "about the different types of artifacts and if design info is"
-                                            " provided then details of stimulus correlated motion and a "
-                                            "listing or artifacts by event type."))
+                                      desc=("One file for each functional run "
+                                            "containing information about the "
+                                            "different types of artifacts and "
+                                            "if design info is provided then "
+                                            "details of stimulus correlated "
+                                            "motion and a listing or artifacts "
+                                            "by event type."))
     plot_files = OutputMultiPath(File,
-                                 desc=("One image file for each functional run containing the "
-                                       "detected outliers"))
+                                 desc=("One image file for each functional run "
+                                       "containing the detected outliers"))
     mask_files = OutputMultiPath(File,
-                                 desc=("One image file for each functional run containing the mask"
-                                       "used for global signal calculation"))
+                                 desc=("One image file for each functional run "
+                                       "containing the mask used for global "
+                                       "signal calculation"))
     displacement_files = OutputMultiPath(File,
-                                         desc=("One image file for each functional run containing the voxel"
-                                               "displacement timeseries"))
+                                         desc=("One image file for each "
+                                               "functional run containing the "
+                                               "voxel displacement timeseries"))
 
 
 class ArtifactDetect(BaseInterface):
@@ -253,7 +278,9 @@ class ArtifactDetect(BaseInterface):
 
     Uses intensity and motion parameters to infer outliers. If `use_norm` is
     True, it computes the movement of the center of each face a cuboid centered
-    around the head and returns the maximal movement across the centers.
+    around the head and returns the maximal movement across the centers. If you
+    wish to use individual thresholds instead, import `Undefined` from
+    `nipype.interfaces.base` and set `....inputs.use_norm = Undefined`
 
 
     Examples
@@ -516,17 +543,19 @@ def _detect_outliers_core(self, imgfile, motionfile, runidx, cwd=None):
                                                          motion_outliers)),
                   'motion_outliers': len(np.setdiff1d(motion_outliers, iidx)),
                   },
-                 {'motion': [{'using differences': self.inputs.use_differences[0]},
-                             {'mean': np.mean(mc_in, axis=0).tolist(),
-                              'min': np.min(mc_in, axis=0).tolist(),
-                              'max': np.max(mc_in, axis=0).tolist(),
-                              'std': np.std(mc_in, axis=0).tolist()},
+                 {'motion': [
+                    {'using differences': self.inputs.use_differences[0]},
+                    {'mean': np.mean(mc_in, axis=0).tolist(),
+                     'min': np.min(mc_in, axis=0).tolist(),
+                     'max': np.max(mc_in, axis=0).tolist(),
+                     'std': np.std(mc_in, axis=0).tolist()},
                              ]},
-                 {'intensity': [{'using differences': self.inputs.use_differences[1]},
-                                {'mean': np.mean(gz, axis=0).tolist(),
-                                 'min': np.min(gz, axis=0).tolist(),
-                                 'max': np.max(gz, axis=0).tolist(),
-                                 'std': np.std(gz, axis=0).tolist()},
+                 {'intensity': [
+                    {'using differences': self.inputs.use_differences[1]},
+                    {'mean': np.mean(gz, axis=0).tolist(),
+                     'min': np.min(gz, axis=0).tolist(),
+                     'max': np.max(gz, axis=0).tolist(),
+                     'std': np.std(gz, axis=0).tolist()},
                                 ]},
                  ]
         if self.inputs.use_norm:
@@ -550,20 +579,27 @@ def _run_interface(self, runtime):
 
 
 class StimCorrInputSpec(BaseInterfaceInputSpec):
-    realignment_parameters = InputMultiPath(File(exists=True), mandatory=True,
-                                            desc=('Names of realignment parameters corresponding to the functional '
-                                                  'data files'))
-    intensity_values = InputMultiPath(File(exists=True), mandatory=True,
-                                      desc='Name of file containing intensity values')
-    spm_mat_file = File(exists=True, mandatory=True,
-                        desc='SPM mat file (use pre-estimate SPM.mat file)')
+    realignment_parameters = InputMultiPath(File(exists=True),
+                                            mandatory=True,
+                                            desc=("Names of realignment "
+                                                  "parameters corresponding to "
+                                                  "the functional data files"))
+    intensity_values = InputMultiPath(File(exists=True),
+                                      mandatory=True,
+                                      desc=("Name of file containing intensity "
+                                           "values"))
+    spm_mat_file = File(exists=True,
+                        mandatory=True,
+                        desc="SPM mat file (use pre-estimate SPM.mat file)")
     concatenated_design = traits.Bool(mandatory=True,
-                                      desc='state if the design matrix contains concatenated sessions')
+                                      desc=("state if the design matrix "
+                                            "contains concatenated sessions"))
 
 
 class StimCorrOutputSpec(TraitedSpec):
     stimcorr_files = OutputMultiPath(File(exists=True),
-                                     desc='List of files containing correlation values')
+                                     desc=("List of files containing "
+                                           "correlation values"))
 
 
 class StimulusCorrelation(BaseInterface):
@@ -573,8 +609,9 @@ class StimulusCorrelation(BaseInterface):
     Currently this class supports an SPM generated design matrix and requires
     intensity parameters. This implies that one must run
     :ref:`ArtifactDetect <nipype.algorithms.rapidart.ArtifactDetect>`
-    and :ref:`Level1Design <nipype.interfaces.spm.model.Level1Design>` prior to running this or
-    provide an SPM.mat file and intensity parameters through some other means.
+    and :ref:`Level1Design <nipype.interfaces.spm.model.Level1Design>` prior to
+    running this or provide an SPM.mat file and intensity parameters through
+    some other means.
 
     Examples
     --------
@@ -650,7 +687,8 @@ def _get_spm_submatrix(self, spmmat, sessidx, rows=None):
         U = spmmat['SPM'][0][0].Sess[0][sessidx].U[0]
         if rows is None:
             rows = spmmat['SPM'][0][0].Sess[0][sessidx].row[0] - 1
-        cols = spmmat['SPM'][0][0].Sess[0][sessidx].col[0][list(range(len(U)))] - 1
+        cols = (
+            spmmat['SPM'][0][0].Sess[0][sessidx].col[0][list(range(len(U)))]-1)
         outmatrix = designmatrix.take(rows.tolist(), axis=0).take(cols.tolist(),
                                                                   axis=1)
         return outmatrix


Test patch: