Repository: nipy/nipype

Issue:
workflow with iterables and cloned nodes fail when expanding iterables
### Summary
When running a workflow which includes a cloned node and iterables the workflow will fail when expanding the iterables because the id of the cloned node will be the same as the original one.

### Actual behavior
Will result in an error:
Traceback (most recent call last):

  File "<ipython-input-55-177d6eaeef2c>", line 27, in <module>
    workflow.run()

  File "/data/eaxfjord/anaconda2/lib/python2.7/site-packages/nipype/pipeline/engine/workflows.py", line 592, in run
    execgraph = generate_expanded_graph(deepcopy(flatgraph))

  File "/data/eaxfjord/anaconda2/lib/python2.7/site-packages/nipype/pipeline/engine/utils.py", line 1042, in generate_expanded_graph
    iterable_prefix, inode.synchronize)

  File "/data/eaxfjord/anaconda2/lib/python2.7/site-packages/nipype/pipeline/engine/utils.py", line 733, in _merge_graphs
    raise Exception(("Execution graph does not have a unique set of node "

Exception: Execution graph does not have a unique set of node names. Please rerun the workflow

### Expected behavior
Will execute normally without the errors.

### How to replicate the behavior
The following workflow will produce the error.

### Script/Workflow details
```python
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe

def addstr(string):
    string = ('%s+2' % string )
    return string

subject_list = ['sub-001', 'sub-002']

inputnode = pe.Node(niu.IdentityInterface(fields = ['subject']),
                        name = 'inputnode')
inputnode.iterables = [('subject', subject_list)]

node_1 = pe.Node(niu.Function(input_names='string',
                              output_names= 'string',
                              function = addstr),name='node_1')
node_2 = node_1.clone('node_2')

workflow = pe.Workflow(name='my_workflow')
workflow.connect([(inputnode, node_1, [('subject','string')]),
                  (node_1, node_2, [('string','string')])])
workflow.run()
```
### Platform details:
/data/eaxfjord/anaconda2/lib/python2.7/site-packages/h5py/__init__.py:34: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters
{'nibabel_version': '2.2.1', 'sys_executable': '/data/eaxfjord/anaconda2/bin/python', 'networkx_version': '2.1', 'numpy_version': '1.14.0', 'sys_platform': 'linux2', 'sys_version': '2.7.13 |Anaconda custom (64-bit)| (default, Dec 20 2016, 23:09:15) \n[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]', 'commit_source': 'installation', 'commit_hash': '0a5948a0', 'pkg_path': '/data/eaxfjord/anaconda2/lib/python2.7/site-packages/nipype', 'nipype_version': '1.0.0', 'traits_version': '4.6.0', 'scipy_version': '1.0.0'}
1.0.0


### Execution environment
- My python environment outside container


Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 08d923467..1e2ed0c3d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,55 +1,64 @@
-cache:
-  apt: true
+dist: trusty
+sudo: required
 
 language: python
+# our build matrix
 python:
 - 2.7
 - 3.4
 - 3.5
 - 3.6
+
 env:
 - INSTALL_DEB_DEPENDECIES=true NIPYPE_EXTRAS="doc,tests,fmri,profiler" CI_SKIP_TEST=1
 - INSTALL_DEB_DEPENDECIES=false NIPYPE_EXTRAS="doc,tests,fmri,profiler" CI_SKIP_TEST=1
 - INSTALL_DEB_DEPENDECIES=true NIPYPE_EXTRAS="doc,tests,fmri,profiler,duecredit" CI_SKIP_TEST=1
 - INSTALL_DEB_DEPENDECIES=true NIPYPE_EXTRAS="doc,tests,fmri,profiler" PIP_FLAGS="--pre" CI_SKIP_TEST=1
+
+addons:
+  apt:
+    packages:
+      - xvfb
+      - fusefat
+      - graphviz
+
+cache:
+  directories:
+    - ${HOME}/.cache
+
 before_install:
-- function apt_inst {
-  if $INSTALL_DEB_DEPENDECIES; then sudo rm -rf /dev/shm; fi &&
-  if $INSTALL_DEB_DEPENDECIES; then sudo ln -s /run/shm /dev/shm; fi &&
-  bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh) &&
-  sudo apt-get -y update &&
-  sudo apt-get -y install xvfb fusefat graphviz &&
-  if $INSTALL_DEB_DEPENDECIES; then travis_retry sudo apt-get install -y -qq
-  fsl afni elastix fsl-atlases; fi &&
-  if $INSTALL_DEB_DEPENDECIES; then
+- if $INSTALL_DEB_DEPENDECIES; then sudo rm -rf /dev/shm; sudo ln -s /run/shm /dev/shm; fi
+- travis_retry bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh);
+- if $INSTALL_DEB_DEPENDECIES; then
+    travis_retry sudo apt-get -y update &&
+    travis_retry sudo apt-get install -y -qq fsl afni elastix fsl-atlases;
+  fi;
+- if $INSTALL_DEB_DEPENDECIES; then
     source /etc/fsl/fsl.sh;
     source /etc/afni/afni.sh;
-    export FSLOUTPUTTYPE=NIFTI_GZ; fi }
-- function conda_inst {
-  export CONDA_HOME=$HOME/conda &&
-  wget https://repo.continuum.io/miniconda/Miniconda${TRAVIS_PYTHON_VERSION:0:1}-latest-Linux-x86_64.sh
-    -O /home/travis/.cache/conda.sh &&
-  bash /home/travis/.cache/conda.sh -b -p ${CONDA_HOME} &&
-  export PATH=${CONDA_HOME}/bin:$PATH &&
-  hash -r &&
-  conda config --set always_yes yes --set changeps1 no &&
-  conda update -q conda &&
-  conda config --add channels conda-forge &&
-  conda install python=${TRAVIS_PYTHON_VERSION} &&
-  conda install -y icu &&
-  pip install -r requirements.txt &&
-  pushd $HOME;
-  git clone https://github.com/INCF/pybids.git;
-  cd pybids;
-  pip install -e .;
-  popd; }
-#  Add install of vtk and mayavi to test mesh (disabled): conda install -y vtk mayavi
-- travis_retry apt_inst
-- travis_retry conda_inst
+    export FSLOUTPUTTYPE=NIFTI_GZ;
+  fi;
+
+# handle python operations separately to reduce timeouts
+- wget https://repo.continuum.io/miniconda/Miniconda${TRAVIS_PYTHON_VERSION:0:1}-latest-Linux-x86_64.sh
+    -O /home/travis/.cache/conda.sh
+- bash ${HOME}/.cache/conda.sh -b -p ${HOME}/conda
+- export PATH=${HOME}/conda/bin:$PATH
+- hash -r
+- conda config --set always_yes yes --set changeps1 no
+- travis_retry conda update -q conda
+- conda config --add channels conda-forge
+- travis_retry conda install -y python=${TRAVIS_PYTHON_VERSION} icu
+- travis_retry pip install -r requirements.txt
+- travis_retry git clone https://github.com/INCF/pybids.git ${HOME}/pybids &&
+  pip install -e ${HOME}/pybids
+
 install:
 - travis_retry pip install $PIP_FLAGS -e .[$NIPYPE_EXTRAS]
+
 script:
 - py.test -v --doctest-modules nipype
+
 deploy:
   provider: pypi
   user: satra
diff --git a/nipype/interfaces/c3.py b/nipype/interfaces/c3.py
index 3a3284b32..f4778e7d9 100644
--- a/nipype/interfaces/c3.py
+++ b/nipype/interfaces/c3.py
@@ -10,9 +10,16 @@
 """
 from __future__ import (print_function, division, unicode_literals,
                         absolute_import)
+import os
+from glob import glob
 
 from .base import (CommandLineInputSpec, traits, TraitedSpec, File,
-                   SEMLikeCommandLine)
+                   SEMLikeCommandLine, InputMultiPath, OutputMultiPath,
+                   CommandLine, isdefined)
+from ..utils.filemanip import split_filename
+from .. import logging
+
+iflogger = logging.getLogger("interface")
 
 
 class C3dAffineToolInputSpec(CommandLineInputSpec):
@@ -52,3 +59,150 @@ class C3dAffineTool(SEMLikeCommandLine):
 
     _cmd = 'c3d_affine_tool'
     _outputs_filenames = {'itk_transform': 'affine.txt'}
+
+
+class C3dInputSpec(CommandLineInputSpec):
+    in_file = InputMultiPath(
+        File(),
+        position=1,
+        argstr="%s",
+        mandatory=True,
+        desc="Input file (wildcard and multiple are supported).")
+    out_file = File(
+        exists=False,
+        argstr="-o %s",
+        position=-1,
+        xor=["out_files"],
+        desc="Output file of last image on the stack.")
+    out_files = InputMultiPath(
+        File(),
+        argstr="-oo %s",
+        xor=["out_file"],
+        position=-1,
+        desc=("Write all images on the convert3d stack as multiple files."
+        " Supports both list of output files or a pattern for the output"
+        " filenames (using %d substituion)."))
+    pix_type = traits.Enum(
+        "float", "char", "uchar", "short", "ushort", "int", "uint", "double",
+        argstr="-type %s",
+        desc=("Specifies the pixel type for the output image. By default,"
+        " images are written in floating point (float) format"))
+    scale = traits.Either(
+        traits.Int(), traits.Float(),
+        argstr="-scale %s",
+        desc=("Multiplies the intensity of each voxel in the last image on the"
+        " stack by the given factor."))
+    shift = traits.Either(
+        traits.Int(), traits.Float(),
+        argstr="-shift %s",
+        desc='Adds the given constant to every voxel.')
+    interp = traits.Enum(
+        "Linear", "NearestNeighbor", "Cubic", "Sinc", "Gaussian",
+        argstr="-interpolation %s",
+        desc=("Specifies the interpolation used with -resample and other"
+        " commands. Default is Linear."))
+    resample = traits.Str(
+        argstr="-resample %s",
+        desc=("Resamples the image, keeping the bounding box the same, but"
+        " changing the number of voxels in the image. The dimensions can be"
+        " specified as a percentage, for example to double the number of voxels"
+        " in each direction. The -interpolation flag affects how sampling is"
+        " performed."))
+    smooth = traits.Str(
+        argstr="-smooth %s",
+        desc=("Applies Gaussian smoothing to the image. The parameter vector"
+        " specifies the standard deviation of the Gaussian kernel."))
+    multicomp_split = traits.Bool(
+        False,
+        usedefault=True,
+        argstr="-mcr",
+        position=0,
+        desc="Enable reading of multi-component images.")
+    is_4d = traits.Bool(
+        False,
+        usedefault=True,
+        desc=("Changes command to support 4D file operations (default is"
+        " false)."))
+
+
+class C3dOutputSpec(TraitedSpec):
+    out_files = OutputMultiPath(File(exists=False))
+
+
+class C3d(CommandLine):
+    """
+    Convert3d is a command-line tool for converting 3D (or 4D) images between
+    common file formats. The tool also includes a growing list of commands for
+    image manipulation, such as thresholding and resampling. The tool can also
+    be used to obtain information about image files. More information on
+    Convert3d can be found at:
+    https://sourceforge.net/p/c3d/git/ci/master/tree/doc/c3d.md
+
+
+    Example
+    =======
+
+    >>> from nipype.interfaces.c3 import C3d
+    >>> c3 = C3d()
+    >>> c3.inputs.in_file = "T1.nii"
+    >>> c3.inputs.pix_type = "short"
+    >>> c3.inputs.out_file = "T1.img"
+    >>> c3.cmdline
+    'c3d T1.nii -type short -o T1.img'
+    >>> c3.inputs.is_4d = True
+    >>> c3.inputs.in_file = "epi.nii"
+    >>> c3.inputs.out_file = "epi.img"
+    >>> c3.cmdline
+    'c4d epi.nii -type short -o epi.img'
+    """
+    input_spec = C3dInputSpec
+    output_spec = C3dOutputSpec
+
+    _cmd = "c3d"
+
+    def __init__(self, **inputs):
+        super(C3d, self).__init__(**inputs)
+        self.inputs.on_trait_change(self._is_4d, "is_4d")
+        if self.inputs.is_4d:
+            self._is_4d()
+
+    def _is_4d(self):
+        self._cmd = "c4d" if self.inputs.is_4d else "c3d"
+
+    def _run_interface(self, runtime):
+        cmd = self._cmd
+        if (not isdefined(self.inputs.out_file)
+            and not isdefined(self.inputs.out_files)):
+            # Convert3d does not want to override file, by default
+            # so we define a new output file
+            self._gen_outfile()
+        runtime = super(C3d, self)._run_interface(runtime)
+        self._cmd = cmd
+        return runtime
+
+    def _gen_outfile(self):
+        # if many infiles, raise exception
+        if (len(self.inputs.in_file) > 1) or ("*" in self.inputs.in_file[0]):
+            raise AttributeError("Multiple in_files found - specify either"
+                                 " `out_file` or `out_files`.")
+        _, fn, ext = split_filename(self.inputs.in_file[0])
+        self.inputs.out_file = fn + "_generated" + ext
+        # if generated file will overwrite, raise error
+        if os.path.exists(os.path.abspath(self.inputs.out_file)):
+            raise IOError("File already found - to overwrite, use `out_file`.")
+        iflogger.info("Generating `out_file`.")
+
+    def _list_outputs(self):
+        outputs = self.output_spec().get()
+        if isdefined(self.inputs.out_file):
+            outputs["out_files"] = os.path.abspath(self.inputs.out_file)
+        if isdefined(self.inputs.out_files):
+            if len(self.inputs.out_files) == 1:
+                _out_files = glob(os.path.abspath(self.inputs.out_files[0]))
+            else:
+                _out_files = [os.path.abspath(f) for f in self.inputs.out_files
+                              if os.path.exists(os.path.abspath(f))]
+            outputs["out_files"] = _out_files
+
+        return outputs
+
diff --git a/nipype/interfaces/mrtrix3/preprocess.py b/nipype/interfaces/mrtrix3/preprocess.py
index ca5996bea..740513194 100644
--- a/nipype/interfaces/mrtrix3/preprocess.py
+++ b/nipype/interfaces/mrtrix3/preprocess.py
@@ -27,7 +27,7 @@ class ResponseSDInputSpec(MRTrix3BaseInputSpec):
         'tournier',
         'tax',
         argstr='%s',
-        position=-6,
+        position=1,
         mandatory=True,
         desc='response estimation algorithm (multi-tissue)')
     in_file = File(
@@ -74,7 +74,7 @@ class ResponseSD(MRTrix3Base):
     >>> resp.inputs.algorithm = 'tournier'
     >>> resp.inputs.grad_fsl = ('bvecs', 'bvals')
     >>> resp.cmdline                               # doctest: +ELLIPSIS
-    'dwi2response -fslgrad bvecs bvals tournier dwi.mif wm.txt'
+    'dwi2response tournier -fslgrad bvecs bvals dwi.mif wm.txt'
     >>> resp.run()                                 # doctest: +SKIP
     """
 
diff --git a/nipype/pipeline/engine/base.py b/nipype/pipeline/engine/base.py
index 51449632b..0883023f6 100644
--- a/nipype/pipeline/engine/base.py
+++ b/nipype/pipeline/engine/base.py
@@ -84,9 +84,12 @@ class EngineBase(object):
             A clone of node or workflow must have a new name
         """
         if name == self.name:
-            raise ValueError('Cloning requires a new name, "%s" is in use.' % name)
+            raise ValueError('Cloning requires a new name, "%s" is '
+                             'in use.' % name)
         clone = deepcopy(self)
         clone.name = name
+        if hasattr(clone, '_id'):
+            clone._id = name
         return clone
 
     def _check_outputs(self, parameter):


Test patch:
diff --git a/nipype/interfaces/mrtrix3/tests/test_auto_ResponseSD.py b/nipype/interfaces/mrtrix3/tests/test_auto_ResponseSD.py
index 29a89f097..01104d2d2 100644
--- a/nipype/interfaces/mrtrix3/tests/test_auto_ResponseSD.py
+++ b/nipype/interfaces/mrtrix3/tests/test_auto_ResponseSD.py
@@ -8,7 +8,7 @@ def test_ResponseSD_inputs():
         algorithm=dict(
             argstr='%s',
             mandatory=True,
-            position=-6,
+            position=1,
         ),
         args=dict(argstr='%s', ),
         bval_scale=dict(argstr='-bvalue_scaling %s', ),
diff --git a/nipype/interfaces/tests/test_auto_C3d.py b/nipype/interfaces/tests/test_auto_C3d.py
new file mode 100644
index 000000000..18300e2af
--- /dev/null
+++ b/nipype/interfaces/tests/test_auto_C3d.py
@@ -0,0 +1,61 @@
+# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
+from __future__ import unicode_literals
+from ..c3 import C3d
+
+
+def test_C3d_inputs():
+    input_map = dict(
+        args=dict(argstr='%s', ),
+        environ=dict(
+            nohash=True,
+            usedefault=True,
+        ),
+        ignore_exception=dict(
+            deprecated='1.0.0',
+            nohash=True,
+            usedefault=True,
+        ),
+        in_file=dict(
+            argstr='%s',
+            mandatory=True,
+            position=1,
+        ),
+        interp=dict(argstr='-interpolation %s', ),
+        is_4d=dict(usedefault=True, ),
+        multicomp_split=dict(
+            argstr='-mcr',
+            position=0,
+            usedefault=True,
+        ),
+        out_file=dict(
+            argstr='-o %s',
+            position=-1,
+            xor=['out_files'],
+        ),
+        out_files=dict(
+            argstr='-oo %s',
+            position=-1,
+            xor=['out_file'],
+        ),
+        pix_type=dict(argstr='-type %s', ),
+        resample=dict(argstr='-resample %s', ),
+        scale=dict(argstr='-scale %s', ),
+        shift=dict(argstr='-shift %s', ),
+        smooth=dict(argstr='-smooth %s', ),
+        terminal_output=dict(
+            deprecated='1.0.0',
+            nohash=True,
+        ),
+    )
+    inputs = C3d.input_spec()
+
+    for key, metadata in list(input_map.items()):
+        for metakey, value in list(metadata.items()):
+            assert getattr(inputs.traits()[key], metakey) == value
+def test_C3d_outputs():
+    output_map = dict(out_files=dict(), )
+    outputs = C3d.output_spec()
+
+    for key, metadata in list(output_map.items()):
+        for metakey, value in list(metadata.items()):
+            assert getattr(outputs.traits()[key], metakey) == value
diff --git a/nipype/pipeline/engine/tests/test_base.py b/nipype/pipeline/engine/tests/test_base.py
index 54356fd6c..fd87aa687 100644
--- a/nipype/pipeline/engine/tests/test_base.py
+++ b/nipype/pipeline/engine/tests/test_base.py
@@ -6,6 +6,8 @@ from __future__ import print_function, unicode_literals
 import pytest
 from ..base import EngineBase
 from ....interfaces import base as nib
+from ....interfaces import utility as niu
+from ... import engine as pe
 
 
 class InputSpec(nib.TraitedSpec):
@@ -64,3 +66,24 @@ def test_clone():
 
     with pytest.raises(ValueError):
         base.clone('nodename')
+
+def test_clone_node_iterables(tmpdir):
+    tmpdir.chdir()
+
+    def addstr(string):
+        return ('%s + 2' % string)
+
+    subject_list = ['sub-001', 'sub-002']
+    inputnode = pe.Node(niu.IdentityInterface(fields=['subject']),
+                            name='inputnode')
+    inputnode.iterables = [('subject', subject_list)]
+
+    node_1 = pe.Node(niu.Function(input_names='string',
+                                  output_names='string',
+                                  function=addstr), name='node_1')
+    node_2 = node_1.clone('node_2')
+
+    workflow = pe.Workflow(name='iter_clone_wf')
+    workflow.connect([(inputnode, node_1, [('subject', 'string')]),
+                      (node_1, node_2, [('string', 'string')])])
+    workflow.run()