Repository: nipy/nipype

Issue:
[BUG] MultiProc crashes are not correctly handled
### Summary
Since the latest refactor, MultiProc calls `self._report_crash` with a `traceback` argument and such function is not defined:

```
File "/usr/local/miniconda/lib/python3.6/site-packages/niworkflows/nipype/pipeline/engine/workflows.py", line 591, in run
runner.run(execgraph, updatehash=updatehash, config=self.config)
File "/usr/local/miniconda/lib/python3.6/site-packages/niworkflows/nipype/pipeline/plugins/base.py", line 182, in run
self._send_procs_to_workers(updatehash=updatehash, graph=graph)
File "/usr/local/miniconda/lib/python3.6/site-packages/niworkflows/nipype/pipeline/plugins/multiproc.py", line 250, in _send_procs_to_workers
self._report_crash(self.procs[jobid], traceback=traceback)
TypeError: _report_crash() got an unexpected keyword argument 'traceback'
```

I'll propose a fix today.

Comments:

Patch:
diff --git a/CHANGES b/CHANGES
index 0845eee28..0480ad874 100644
--- a/CHANGES
+++ b/CHANGES
@@ -7,6 +7,7 @@ Upcoming release
 
 ###### [Full changelog](https://github.com/nipy/nipype/milestone/13)
 
+* FIX: MultiProc mishandling crashes (https://github.com/nipy/nipype/pull/2301)
 * MAINT: Revise use of `subprocess.Popen` (https://github.com/nipy/nipype/pull/2289)
 * ENH: Memorize version checks (https://github.com/nipy/nipype/pull/2274, https://github.com/nipy/nipype/pull/2295)
 
diff --git a/nipype/interfaces/ants/registration.py b/nipype/interfaces/ants/registration.py
index 6d82a2e9f..785205952 100644
--- a/nipype/interfaces/ants/registration.py
+++ b/nipype/interfaces/ants/registration.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
-"""The ants module provides basic functions for interfacing with ants functions.
+"""The ants module provides basic functions for interfacing with ants
+   functions.
 
    Change directory to provide relative paths for doctests
    >>> import os
@@ -7,7 +8,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 range, str
 import os
 
@@ -20,17 +22,19 @@ class ANTSInputSpec(ANTSCommandInputSpec):
     dimension = traits.Enum(3, 2, argstr='%d', usedefault=False,
                             position=1, desc='image dimension (2 or 3)')
     fixed_image = InputMultiPath(File(exists=True), mandatory=True,
-                                 desc=('image to which the moving image is warped'))
+                                 desc=('image to which the moving image is '
+                                       'warped'))
     moving_image = InputMultiPath(File(exists=True), argstr='%s',
                                   mandatory=True,
-                                  desc=('image to apply transformation to (generally a coregistered '
+                                  desc=('image to apply transformation to '
+                                        '(generally a coregistered'
                                         'functional)'))
 
 #    Not all metrics are appropriate for all modalities. Also, not all metrics
-#    are efficeint or appropriate at all resolution levels, Some metrics perform
-#    well for gross global registraiton, but do poorly for small changes (i.e.
-#    Mattes), and some metrics do well for small changes but don't work well for
-#    gross level changes (i.e. 'CC').
+#    are efficeint or appropriate at all resolution levels, Some metrics
+#    perform well for gross global registraiton, but do poorly for small
+#    changes (i.e. Mattes), and some metrics do well for small changes but
+#    don't work well for gross level changes (i.e. 'CC').
 #
 #    This is a two stage registration. in the first stage
 #      [ 'Mattes', .................]
@@ -49,10 +53,18 @@ class ANTSInputSpec(ANTSCommandInputSpec):
     metric = traits.List(traits.Enum('CC', 'MI', 'SMI', 'PR', 'SSD',
                                      'MSQ', 'PSE'), mandatory=True, desc='')
 
-    metric_weight = traits.List(traits.Float(), requires=['metric'], desc='')
-    radius = traits.List(traits.Int(), requires=['metric'], desc='')
+    metric_weight = traits.List(traits.Float(), value=[1.0], usedefault=True,
+                                requires=['metric'], mandatory=True,
+                                desc='the metric weight(s) for each stage. '
+                                'The weights must sum to 1 per stage.')
 
-    output_transform_prefix = Str('out', usedefault=True, argstr='--output-naming %s',
+    radius = traits.List(traits.Int(), requires=['metric'],  mandatory=True,
+                         desc='radius of the region (i.e. number of layers'
+                         ' around a voxel point)'
+                         ' that is used for computing cross correlation')
+
+    output_transform_prefix = Str('out', usedefault=True,
+                                  argstr='--output-naming %s',
                                   mandatory=True, desc='')
     transformation_model = traits.Enum('Diff', 'Elast', 'Exp', 'Greedy Exp',
                                        'SyN', argstr='%s', mandatory=True,
diff --git a/nipype/pipeline/plugins/multiproc.py b/nipype/pipeline/plugins/multiproc.py
index 16bfb51a0..b26d02951 100644
--- a/nipype/pipeline/plugins/multiproc.py
+++ b/nipype/pipeline/plugins/multiproc.py
@@ -238,8 +238,10 @@ class MultiProcPlugin(DistributedPluginBase):
                     num_subnodes = self.procs[jobid].num_subnodes()
                 except Exception:
                     traceback = format_exception(*sys.exc_info())
-                    self._report_crash(self.procs[jobid], traceback=traceback)
-                    self._clean_queue(jobid, graph)
+                    self._clean_queue(
+                        jobid, graph,
+                        result={'result': None, 'traceback': traceback}
+                    )
                     self.proc_pending[jobid] = False
                     continue
                 if num_subnodes > 1:
@@ -275,10 +277,13 @@ class MultiProcPlugin(DistributedPluginBase):
                 logger.debug('Running node %s on master thread',
                              self.procs[jobid])
                 try:
-                    self.procs[jobid].run()
+                    self.procs[jobid].run(updatehash=updatehash)
                 except Exception:
                     traceback = format_exception(*sys.exc_info())
-                    self._report_crash(self.procs[jobid], traceback=traceback)
+                    self._clean_queue(
+                        jobid, graph,
+                        result={'result': None, 'traceback': traceback}
+                    )
 
                 # Release resources
                 self._task_finished_cb(jobid)


Test patch:
diff --git a/nipype/interfaces/ants/tests/test_registration.py b/nipype/interfaces/ants/tests/test_registration.py
new file mode 100644
index 000000000..745b825c6
--- /dev/null
+++ b/nipype/interfaces/ants/tests/test_registration.py
@@ -0,0 +1,22 @@
+# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
+# vi: set ft=python sts=4 ts=4 sw=4 et:
+from __future__ import unicode_literals
+from nipype.interfaces.ants import registration
+import os
+import pytest
+
+
+def test_ants_mand(tmpdir):
+    tmpdir.chdir()
+    filepath = os.path.dirname(os.path.realpath(__file__))
+    datadir = os.path.realpath(os.path.join(filepath, '../../../testing/data'))
+
+    ants = registration.ANTS()
+    ants.inputs.transformation_model = "SyN"
+    ants.inputs.moving_image = [os.path.join(datadir, 'resting.nii')]
+    ants.inputs.fixed_image = [os.path.join(datadir, 'T1.nii')]
+    ants.inputs.metric = ['MI']
+
+    with pytest.raises(ValueError) as er:
+        ants.run()
+    assert "ANTS requires a value for input 'radius'" in str(er.value)