Repository: nipy/nipype

Issue:
MRtrix 3.0 `dwi2fod` single output error
Hi, I got this error when executing `mrtrix3.EstimateFOD` interface with the latest MRtrix version (`3.0_RC2-117-gf098f097`) installed on macOS Mojave 10.14.2 through a `brew install mrtrix3`:

```
dwi2fod: [ERROR] CSD algorithm expects a single input response function and single output FOD image
```

It seems that MRtrix now expects "pairs of input tissue response and output ODF images" according to [the documentation](https://mrtrix.readthedocs.io/en/latest/reference/commands/dwi2fod.html#usage). So I guess that the following lines need to be updated:

https://github.com/nipy/nipype/blob/35b58cdb59a900430f4b9639e220c8408341805d/nipype/interfaces/mrtrix3/reconst.py#L95-L109
[FIX] Mrtrix3 usedefault issue
## Summary

Fixes #2822 .

## List of changes proposed in this PR (pull-request)

* Set `useDefault=False` for `gm_odf` and `csf_odf` in order to avoid issues with the `csd` algorithm;
* Removed `useDefault=True` for the `-lmax` argument in order to avoid issues in case of multi-shell data.

## Acknowledgment

I acknowledge that this contribution will be available under the Apache 2 license.



Discussion:

Resolving patch:
diff --git a/nipype/interfaces/mrtrix3/__init__.py b/nipype/interfaces/mrtrix3/__init__.py
--- a/nipype/interfaces/mrtrix3/__init__.py
+++ b/nipype/interfaces/mrtrix3/__init__.py
@@ -23,5 +23,5 @@
     DWIBiasCorrect,
 )
 from .tracking import Tractography
-from .reconst import FitTensor, EstimateFOD
+from .reconst import FitTensor, EstimateFOD, ConstrainedSphericalDeconvolution
 from .connectivity import LabelConfig, LabelConvert, BuildConnectome
diff --git a/nipype/interfaces/mrtrix3/reconst.py b/nipype/interfaces/mrtrix3/reconst.py
--- a/nipype/interfaces/mrtrix3/reconst.py
+++ b/nipype/interfaces/mrtrix3/reconst.py
@@ -156,18 +156,24 @@ class EstimateFOD(MRTrix3Base):
     """
     Estimate fibre orientation distributions from diffusion data using spherical deconvolution
 
+    .. warning::
+
+       The CSD algorithm does not work as intended, but fixing it in this interface could break
+       existing workflows. This interface has been superseded by
+       :py:class:`.ConstrainedSphericalDecomposition`.
+
     Example
     -------
 
     >>> import nipype.interfaces.mrtrix3 as mrt
     >>> fod = mrt.EstimateFOD()
-    >>> fod.inputs.algorithm = 'csd'
+    >>> fod.inputs.algorithm = 'msmt_csd'
     >>> fod.inputs.in_file = 'dwi.mif'
     >>> fod.inputs.wm_txt = 'wm.txt'
     >>> fod.inputs.grad_fsl = ('bvecs', 'bvals')
-    >>> fod.cmdline                               # doctest: +ELLIPSIS
-    'dwi2fod -fslgrad bvecs bvals -lmax 8 csd dwi.mif wm.txt wm.mif gm.mif csf.mif'
-    >>> fod.run()                                 # doctest: +SKIP
+    >>> fod.cmdline
+    'dwi2fod -fslgrad bvecs bvals -lmax 8 msmt_csd dwi.mif wm.txt wm.mif gm.mif csf.mif'
+    >>> fod.run()  # doctest: +SKIP
     """
 
     _cmd = "dwi2fod"
@@ -182,3 +188,46 @@ def _list_outputs(self):
         if self.inputs.csf_odf != Undefined:
             outputs["csf_odf"] = op.abspath(self.inputs.csf_odf)
         return outputs
+
+
+class ConstrainedSphericalDeconvolutionInputSpec(EstimateFODInputSpec):
+    gm_odf = File(argstr="%s", position=-3, desc="output GM ODF")
+    csf_odf = File(argstr="%s", position=-1, desc="output CSF ODF")
+    max_sh = InputMultiObject(
+        traits.Int,
+        argstr="-lmax %s",
+        sep=",",
+        desc=(
+            "maximum harmonic degree of response function - single value for single-shell response, list for multi-shell response"
+        ),
+    )
+
+
+class ConstrainedSphericalDeconvolution(EstimateFOD):
+    """
+    Estimate fibre orientation distributions from diffusion data using spherical deconvolution
+
+    This interface supersedes :py:class:`.EstimateFOD`.
+    The old interface has contained a bug when using the CSD algorithm as opposed to the MSMT CSD
+    algorithm, but fixing it could potentially break existing workflows. The new interface works
+    the same, but does not populate the following inputs by default:
+
+    * ``gm_odf``
+    * ``csf_odf``
+    * ``max_sh``
+
+    Example
+    -------
+
+    >>> import nipype.interfaces.mrtrix3 as mrt
+    >>> fod = mrt.ConstrainedSphericalDeconvolution()
+    >>> fod.inputs.algorithm = 'csd'
+    >>> fod.inputs.in_file = 'dwi.mif'
+    >>> fod.inputs.wm_txt = 'wm.txt'
+    >>> fod.inputs.grad_fsl = ('bvecs', 'bvals')
+    >>> fod.cmdline
+    'dwi2fod -fslgrad bvecs bvals csd dwi.mif wm.txt wm.mif'
+    >>> fod.run()  # doctest: +SKIP
+    """
+
+    input_spec = ConstrainedSphericalDeconvolutionInputSpec
diff --git a/tools/checkspecs.py b/tools/checkspecs.py
--- a/tools/checkspecs.py
+++ b/tools/checkspecs.py
@@ -324,7 +324,7 @@ def test_specs(self, uri):
                     and "xor" not in trait.__dict__
                 ):
                     if (
-                        trait.trait_type.__class__.__name__ is "Range"
+                        trait.trait_type.__class__.__name__ == "Range"
                         and trait.default == trait.trait_type._low
                     ):
                         continue


Test patch:
diff --git a/nipype/interfaces/mrtrix3/tests/test_auto_ConstrainedSphericalDeconvolution.py b/nipype/interfaces/mrtrix3/tests/test_auto_ConstrainedSphericalDeconvolution.py
new file mode 100644
--- /dev/null
+++ b/nipype/interfaces/mrtrix3/tests/test_auto_ConstrainedSphericalDeconvolution.py
@@ -0,0 +1,47 @@
+# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
+from ..reconst import ConstrainedSphericalDeconvolution
+
+
+def test_ConstrainedSphericalDeconvolution_inputs():
+    input_map = dict(
+        algorithm=dict(argstr="%s", mandatory=True, position=-8,),
+        args=dict(argstr="%s",),
+        bval_scale=dict(argstr="-bvalue_scaling %s",),
+        csf_odf=dict(argstr="%s", extensions=None, position=-1,),
+        csf_txt=dict(argstr="%s", extensions=None, position=-2,),
+        environ=dict(nohash=True, usedefault=True,),
+        gm_odf=dict(argstr="%s", extensions=None, position=-3,),
+        gm_txt=dict(argstr="%s", extensions=None, position=-4,),
+        grad_file=dict(argstr="-grad %s", extensions=None, xor=["grad_fsl"],),
+        grad_fsl=dict(argstr="-fslgrad %s %s", xor=["grad_file"],),
+        in_bval=dict(extensions=None,),
+        in_bvec=dict(argstr="-fslgrad %s %s", extensions=None,),
+        in_dirs=dict(argstr="-directions %s", extensions=None,),
+        in_file=dict(argstr="%s", extensions=None, mandatory=True, position=-7,),
+        mask_file=dict(argstr="-mask %s", extensions=None,),
+        max_sh=dict(argstr="-lmax %s", sep=",",),
+        nthreads=dict(argstr="-nthreads %d", nohash=True,),
+        shell=dict(argstr="-shell %s", sep=",",),
+        wm_odf=dict(
+            argstr="%s", extensions=None, mandatory=True, position=-5, usedefault=True,
+        ),
+        wm_txt=dict(argstr="%s", extensions=None, mandatory=True, position=-6,),
+    )
+    inputs = ConstrainedSphericalDeconvolution.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_ConstrainedSphericalDeconvolution_outputs():
+    output_map = dict(
+        csf_odf=dict(argstr="%s", extensions=None,),
+        gm_odf=dict(argstr="%s", extensions=None,),
+        wm_odf=dict(argstr="%s", extensions=None,),
+    )
+    outputs = ConstrainedSphericalDeconvolution.output_spec()
+
+    for key, metadata in list(output_map.items()):
+        for metakey, value in list(metadata.items()):
+            assert getattr(outputs.traits()[key], metakey) == value