Repository: nipy/nipype

Issue:
Voxelwise dvars explodes with voxels with variance ~1e-19
### Summary

In `compute_dvars` we remove zero-variance voxels:

https://github.com/nipy/nipype/blob/3e7e61339ee7d0e67c917477fa989759fa69a577/nipype/algorithms/confounds.py#L1045-L1054

However, if `func_sd` is small but nonzero, this will propagate to `diff_sdhat`:

https://github.com/nipy/nipype/blob/3e7e61339ee7d0e67c917477fa989759fa69a577/nipype/algorithms/confounds.py#L1061-L1063

Which then can explode the values in 

https://github.com/nipy/nipype/blob/3e7e61339ee7d0e67c917477fa989759fa69a577/nipype/algorithms/confounds.py#L1078-L1080

If sufficiently small, we will overflow `float32`.

### Proposal

We should set a default threshold for "zerovariance". I think 1e-10 is more than small enough. In the example dataset I have, only one additional voxel is caught by this less stringent threshold, and would avoid the failure.

Will propose a fix soon, but wanted to give people a chance to comment.


Discussion:

Resolving patch:
diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py
--- a/nipype/algorithms/confounds.py
+++ b/nipype/algorithms/confounds.py
@@ -50,6 +50,11 @@ class ComputeDVARSInputSpec(BaseInterfaceInputSpec):
     remove_zerovariance = traits.Bool(
         True, usedefault=True, desc="remove voxels with zero variance"
     )
+    variance_tol = traits.Float(
+        1e-7,
+        usedefault=True,
+        desc="maximum variance to consider \"close to\" zero for the purposes of removal",
+    )
     save_std = traits.Bool(True, usedefault=True, desc="save standardized DVARS")
     save_nstd = traits.Bool(False, usedefault=True, desc="save non-standardized DVARS")
     save_vxstd = traits.Bool(
@@ -167,6 +172,7 @@ def _run_interface(self, runtime):
             self.inputs.in_file,
             self.inputs.in_mask,
             remove_zerovariance=self.inputs.remove_zerovariance,
+            variance_tol=self.inputs.variance_tol,
             intensity_normalization=self.inputs.intensity_normalization,
         )
 
@@ -994,8 +1000,19 @@ def _list_outputs(self):
         return self._results
 
 
+def _AR_est_YW(x, order, rxx=None):
+    """Retrieve AR coefficients while dropping the sig_sq return value"""
+    from nitime.algorithms import AR_est_YW
+
+    return AR_est_YW(x, order, rxx=rxx)[0]
+
+
 def compute_dvars(
-    in_file, in_mask, remove_zerovariance=False, intensity_normalization=1000
+    in_file,
+    in_mask,
+    remove_zerovariance=False,
+    intensity_normalization=1000,
+    variance_tol=0.0,
 ):
     """
     Compute the :abbr:`DVARS (D referring to temporal
@@ -1027,17 +1044,15 @@ def compute_dvars(
     """
     import numpy as np
     import nibabel as nb
-    from nitime.algorithms import AR_est_YW
     import warnings
 
-    func = nb.load(in_file).get_fdata(dtype=np.float32)
-    mask = np.asanyarray(nb.load(in_mask).dataobj).astype(np.uint8)
+    func = np.float32(nb.load(in_file).dataobj)
+    mask = np.bool_(nb.load(in_mask).dataobj)
 
     if len(func.shape) != 4:
         raise RuntimeError("Input fMRI dataset should be 4-dimensional")
 
-    idx = np.where(mask > 0)
-    mfunc = func[idx[0], idx[1], idx[2], :]
+    mfunc = func[mask]
 
     if intensity_normalization != 0:
         mfunc = (mfunc / np.median(mfunc)) * intensity_normalization
@@ -1045,18 +1060,19 @@ def compute_dvars(
     # Robust standard deviation (we are using "lower" interpolation
     # because this is what FSL is doing
     func_sd = (
-        np.percentile(mfunc, 75, axis=1, interpolation="lower")
-        - np.percentile(mfunc, 25, axis=1, interpolation="lower")
+        np.percentile(mfunc, 75, axis=1, method="lower")
+        - np.percentile(mfunc, 25, axis=1, method="lower")
     ) / 1.349
 
     if remove_zerovariance:
-        mfunc = mfunc[func_sd != 0, :]
-        func_sd = func_sd[func_sd != 0]
+        zero_variance_voxels = func_sd > variance_tol
+        mfunc = mfunc[zero_variance_voxels, :]
+        func_sd = func_sd[zero_variance_voxels]
 
     # Compute (non-robust) estimate of lag-1 autocorrelation
     ar1 = np.apply_along_axis(
-        AR_est_YW, 1, regress_poly(0, mfunc, remove_mean=True)[0].astype(np.float32), 1
-    )[:, 0]
+        _AR_est_YW, 1, regress_poly(0, mfunc, remove_mean=True)[0].astype(np.float32), 1
+    )
 
     # Compute (predicted) standard deviation of temporal difference time series
     diff_sdhat = np.squeeze(np.sqrt(((1 - ar1) * 2).tolist())) * func_sd


Test patch:
diff --git a/nipype/algorithms/tests/test_auto_ComputeDVARS.py b/nipype/algorithms/tests/test_auto_ComputeDVARS.py
--- a/nipype/algorithms/tests/test_auto_ComputeDVARS.py
+++ b/nipype/algorithms/tests/test_auto_ComputeDVARS.py
@@ -43,6 +43,9 @@ def test_ComputeDVARS_inputs():
             usedefault=True,
         ),
         series_tr=dict(),
+        variance_tol=dict(
+            usedefault=True,
+        ),
     )
     inputs = ComputeDVARS.input_spec()