Repository: scipy/scipy

Issue:
Docstring missing defaults
metric and sampling argument defaults don't have their defaults documented.
https://github.com/scipy/scipy/blob/adc4f4f7bab120ccfab9383aba272954a0a12fb0/scipy/ndimage/morphology.py#L1813-L1947


Discussion:
Hi @terrdavis . The sampling doesn't have any defaults. And the metric default is given in the function signature. We can probably squeeze one more sentence to make it more obvious. Care to send a PR? 
Sure. I also noticed that the documentation for the distance_transform_* family is inconsistent, is it advisable to make them consistent while I'm at it? 
Sure, the more consistent the merrier.
The documentation and behavior don't appear to be consistent. 

The behavior is to not return results that have output container specified, but the docs don't mention anything about this. Should I update the documentation to reflect the behavior or visa-versa?
Also, the return_distances and return_indicies flags imply that the results are returned regardless of whether an output container is supplied.

I don't use numpy/scipy a lot; is just a convention that I'm unaware of?

Resolving patch:
diff --git a/scipy/ndimage/morphology.py b/scipy/ndimage/morphology.py
--- a/scipy/ndimage/morphology.py
+++ b/scipy/ndimage/morphology.py
@@ -1850,12 +1850,13 @@ def distance_transform_bf(input, metric="euclidean", sampling=None,
     distances : ndarray, optional
         An output array to store the calculated distance transform, instead of 
         returning it.
-        This parameter is only used when `return_distances` is True.
+        `return_distances` must be True.
         It must be the same shape as `input`, and of type float64 if `metric`
         is 'euclidean', uint32 otherwise.
     indices : int32 ndarray, optional
-        An output array to store the calculated feature transform, instead of 
+        An output array to store the calculated feature transform, instead of
         returning it.
+        `return_indicies` must be True.
         Its shape must be `(input.ndim,) + input.shape`.
 
     Returns
@@ -1865,10 +1866,10 @@ def distance_transform_bf(input, metric="euclidean", sampling=None,
         `return_distances` is True and `distances` is not supplied.
         It will have the same shape as the input array.
     indices : int32 ndarray, optional
-        The calculated feature transform. It has an input-shaped array for each 
-        dimension of the input. See distance_transform_edt documentation for an 
+        The calculated feature transform. It has an input-shaped array for each
+        dimension of the input. See distance_transform_edt documentation for an
         example.
-        Returned only when `return_indices` is True and `indices` is not 
+        Returned only when `return_indices` is True and `indices` is not
         supplied.
 
     Notes
@@ -1878,9 +1879,11 @@ def distance_transform_bf(input, metric="euclidean", sampling=None,
     chessboard algorithms.
 
     """
-    if (not return_distances) and (not return_indices):
-        msg = 'at least one of distances/indices must be specified'
-        raise RuntimeError(msg)
+    ft_inplace = isinstance(indices, numpy.ndarray)
+    dt_inplace = isinstance(distances, numpy.ndarray)
+    _distance_tranform_arg_check(
+        dt_inplace, ft_inplace, return_distances, return_indices
+    )
 
     tmp1 = numpy.asarray(input) != 0
     struct = generate_binary_structure(tmp1.ndim, tmp1.ndim)
@@ -1928,9 +1931,9 @@ def distance_transform_bf(input, metric="euclidean", sampling=None,
     if return_indices:
         if isinstance(indices, numpy.ndarray):
             if indices.dtype.type != numpy.int32:
-                raise RuntimeError('indices must of int32 type')
+                raise RuntimeError('indices array must be int32')
             if indices.shape != (tmp1.ndim,) + tmp1.shape:
-                raise RuntimeError('indices has wrong shape')
+                raise RuntimeError('indices array has wrong shape')
             tmp2 = indices
         else:
             tmp2 = numpy.indices(tmp1.shape, dtype=numpy.int32)
@@ -1943,9 +1946,9 @@ def distance_transform_bf(input, metric="euclidean", sampling=None,
 
     # construct and return the result
     result = []
-    if return_distances and not isinstance(distances, numpy.ndarray):
+    if return_distances and not dt_inplace:
         result.append(dt)
-    if return_indices and not isinstance(indices, numpy.ndarray):
+    if return_indices and not ft_inplace:
         result.append(ft)
 
     if len(result) == 2:
@@ -1960,7 +1963,7 @@ def distance_transform_cdt(input, metric='chessboard', return_distances=True,
                            return_indices=False, distances=None, indices=None):
     """
     Distance transform for chamfer type of transforms.
-    
+
     In addition to the distance transform, the feature transform can
     be calculated. In this case the index of the closest background
     element to each foreground element is returned in a separate array.
@@ -1989,13 +1992,14 @@ def distance_transform_cdt(input, metric='chessboard', return_distances=True,
         Whether to calculate the feature transform.
         Default is False.
     distances : int32 ndarray, optional
-        An output array to store the calculated distance transform, instead of 
+        An output array to store the calculated distance transform, instead of
         returning it.
-        This parameter is only used when `return_distances` is True.
+        `return_distances` must be True.
         It must be the same shape as `input`.
     indices : int32 ndarray, optional
-        An output array to store the calculated feature transform, instead of 
+        An output array to store the calculated feature transform, instead of
         returning it.
+        `return_indicies` must be True.
         Its shape must be `(input.ndim,) + input.shape`.
 
     Returns
@@ -2005,19 +2009,18 @@ def distance_transform_cdt(input, metric='chessboard', return_distances=True,
         `return_distances` is True, and `distances` is not supplied.
         It will have the same shape as the input array.
     indices : int32 ndarray, optional
-        The calculated feature transform. It has an input-shaped array for each 
-        dimension of the input. See distance_transform_edt documentation for an 
+        The calculated feature transform. It has an input-shaped array for each
+        dimension of the input. See distance_transform_edt documentation for an
         example.
-        Returned only when `return_indices` is True, and `indices` is not 
+        Returned only when `return_indices` is True, and `indices` is not
         supplied.
 
     """
-    if (not return_distances) and (not return_indices):
-        msg = 'at least one of distances/indices must be specified'
-        raise RuntimeError(msg)
-
     ft_inplace = isinstance(indices, numpy.ndarray)
     dt_inplace = isinstance(distances, numpy.ndarray)
+    _distance_tranform_arg_check(
+        dt_inplace, ft_inplace, return_distances, return_indices
+    )
     input = numpy.asarray(input)
     if metric in ['taxicab', 'cityblock', 'manhattan']:
         rank = input.ndim
@@ -2065,9 +2068,9 @@ def distance_transform_cdt(input, metric='chessboard', return_distances=True,
         ft = numpy.ravel(ft)
         if ft_inplace:
             if indices.dtype.type != numpy.int32:
-                raise RuntimeError('indices must of int32 type')
+                raise RuntimeError('indices array must be int32')
             if indices.shape != (dt.ndim,) + dt.shape:
-                raise RuntimeError('indices has wrong shape')
+                raise RuntimeError('indices array has wrong shape')
             tmp = indices
         else:
             tmp = numpy.indices(dt.shape, dtype=numpy.int32)
@@ -2117,13 +2120,14 @@ def distance_transform_edt(input, sampling=None, return_distances=True,
         Whether to calculate the feature transform.
         Default is False.
     distances : float64 ndarray, optional
-        An output array to store the calculated distance transform, instead of 
+        An output array to store the calculated distance transform, instead of
         returning it.
-        This parameter is only used when `return_distances` is True.
+        `return_distances` must be True.
         It must be the same shape as `input`.
     indices : int32 ndarray, optional
-        An output array to store the calculated feature transform, instead of 
+        An output array to store the calculated feature transform, instead of
         returning it.
+        `return_indicies` must be True.
         Its shape must be `(input.ndim,) + input.shape`.
 
     Returns
@@ -2133,9 +2137,9 @@ def distance_transform_edt(input, sampling=None, return_distances=True,
         `return_distances` is True and `distances` is not supplied.
         It will have the same shape as the input array.
     indices : int32 ndarray, optional
-        The calculated feature transform. It has an input-shaped array for each 
+        The calculated feature transform. It has an input-shaped array for each
         dimension of the input. See example below.
-        Returned only when `return_indices` is True and `indices` is not 
+        Returned only when `return_indices` is True and `indices` is not
         supplied.
 
     Notes
@@ -2212,12 +2216,12 @@ def distance_transform_edt(input, sampling=None, return_distances=True,
             [0, 0, 3, 3, 4]]])
 
     """
-    if (not return_distances) and (not return_indices):
-        msg = 'at least one of distances/indices must be specified'
-        raise RuntimeError(msg)
-
     ft_inplace = isinstance(indices, numpy.ndarray)
     dt_inplace = isinstance(distances, numpy.ndarray)
+    _distance_tranform_arg_check(
+        dt_inplace, ft_inplace, return_distances, return_indices
+    )
+
     # calculate the feature transform
     input = numpy.atleast_1d(numpy.where(input, 1, 0).astype(numpy.int8))
     if sampling is not None:
@@ -2229,9 +2233,9 @@ def distance_transform_edt(input, sampling=None, return_distances=True,
     if ft_inplace:
         ft = indices
         if ft.shape != (input.ndim,) + input.shape:
-            raise RuntimeError('indices has wrong shape')
+            raise RuntimeError('indices array has wrong shape')
         if ft.dtype.type != numpy.int32:
-            raise RuntimeError('indices must be of int32 type')
+            raise RuntimeError('indices array must be int32')
     else:
         ft = numpy.zeros((input.ndim,) + input.shape, dtype=numpy.int32)
 
@@ -2247,9 +2251,9 @@ def distance_transform_edt(input, sampling=None, return_distances=True,
         if dt_inplace:
             dt = numpy.add.reduce(dt, axis=0)
             if distances.shape != dt.shape:
-                raise RuntimeError('distances has wrong shape')
+                raise RuntimeError('distances array has wrong shape')
             if distances.dtype.type != numpy.float64:
-                raise RuntimeError('distances must be of float64 type')
+                raise RuntimeError('distances array must be float64')
             numpy.sqrt(dt, distances)
         else:
             dt = numpy.add.reduce(dt, axis=0)
@@ -2268,3 +2272,20 @@ def distance_transform_edt(input, sampling=None, return_distances=True,
         return result[0]
     else:
         return None
+
+
+def _distance_tranform_arg_check(distances_out, indices_out,
+                                 return_distances, return_indices):
+    """Raise a RuntimeError if the arguments are invalid"""
+    error_msgs = []
+    if (not return_distances) and (not return_indices):
+        error_msgs.append(
+            'at least one of return_distances/return_indices must be True')
+    if distances_out and not return_distances:
+        error_msgs.append(
+            'return_distances must be True if distances is supplied'
+        )
+    if indices_out and not return_indices:
+        error_msgs.append('return_indices must be True if indices is supplied')
+    if error_msgs:
+        raise RuntimeError(', '.join(error_msgs))


Test patch:
diff --git a/scipy/ndimage/tests/test_morphology.py b/scipy/ndimage/tests/test_morphology.py
--- a/scipy/ndimage/tests/test_morphology.py
+++ b/scipy/ndimage/tests/test_morphology.py
@@ -283,6 +283,22 @@ def test_distance_transform_bf06(self, dtype):
                      [0, 1, 2, 3, 4, 5, 6, 7, 8]]]
         assert_array_almost_equal(ft, expected)
 
+    def test_distance_transform_bf07(self):
+        # test input validation per discussion on PR #13302
+        data = numpy.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0]])
+        with assert_raises(RuntimeError):
+            ndimage.distance_transform_bf(
+                data, return_distances=False, return_indices=False
+            )
+
     @pytest.mark.parametrize('dtype', types)
     def test_distance_transform_cdt01(self, dtype):
         # chamfer type distance (cdt) transform
@@ -405,6 +421,26 @@ def test_distance_transform_cdt03(self, dtype):
         for ft in fts:
             assert_array_almost_equal(tft, ft)
 
+    def test_distance_transform_cdt04(self):
+        # test input validation per discussion on PR #13302
+        data = numpy.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0]])
+        indices_out = numpy.zeros((data.ndim,) + data.shape, dtype=numpy.int32)
+        with assert_raises(RuntimeError):
+            ndimage.distance_transform_bf(
+                data,
+                return_distances=True,
+                return_indices=False,
+                indices=indices_out
+            )
+
     @pytest.mark.parametrize('dtype', types)
     def test_distance_transform_edt01(self, dtype):
         # euclidean distance transform (edt)
@@ -513,6 +549,26 @@ def test_distance_transform_edt5(self):
         out = ndimage.distance_transform_edt(False)
         assert_array_almost_equal(out, [0.])
 
+    def test_distance_transform_edt6(self):
+        # test input validation per discussion on PR #13302
+        data = numpy.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 1, 1, 1, 1, 1, 0, 0],
+                            [0, 0, 0, 1, 1, 1, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0],
+                            [0, 0, 0, 0, 0, 0, 0, 0, 0]])
+        distances_out = numpy.zeros(data.shape, dtype=numpy.float64)
+        with assert_raises(RuntimeError):
+            ndimage.distance_transform_bf(
+                data,
+                return_indices=True,
+                return_distances=False,
+                distances=distances_out
+            )
+
     def test_generate_structure01(self):
         struct = ndimage.generate_binary_structure(0, 1)
         assert_array_almost_equal(struct, 1)