Repository: cupy/cupy

Issue:
IndexError when convolving with float scalar
# My setup
```
OS                           : Linux-5.4.0-80-generic-x86_64-with-glibc2.31
Python Version               : 3.9.5
CuPy Version                 : 9.2.0
CuPy Platform                : NVIDIA CUDA
NumPy Version                : 1.21.0
SciPy Version                : 1.7.0
Cython Build Version         : 0.29.23
Cython Runtime Version       : 0.29.23
CUDA Root                    : /usr/local/cuda
nvcc PATH                    : /usr/local/cuda/bin/nvcc
CUDA Build Version           : 11000
CUDA Driver Version          : 11020
CUDA Runtime Version         : 11000
cuBLAS Version               : (available)
cuFFT Version                : 10200
cuRAND Version               : 10201
cuSOLVER Version             : (10, 5, 0)
cuSPARSE Version             : (available)
NVRTC Version                : (11, 0)
Thrust Version               : 100909
CUB Build Version            : 100909
Jitify Build Version         : <unknown>
cuDNN Build Version          : 7605
cuDNN Version                : 7605
NCCL Build Version           : None
NCCL Runtime Version         : None
cuTENSOR Version             : None
cuSPARSELt Build Version     : None
Device 0 Name                : GeForce GTX 1060 6GB
Device 0 Compute Capability  : 61
Device 0 PCI Bus ID          : 0000:01:00.0
```

Cupy throws Index error when convolving an array with only one value, if the dtype is not an integer.
See the following reproducer:

```python
import cupy as cp
import cupyx.scipy.signal as csig
import scipy.signal as sig

a = cp.arange(10)
b = cp.ones(1)

print("This works - scipy")
print(sig.convolve(a.get(),b.get(), mode='full'))


print("This works as well - cupy")
print(csig.convolve(a, b, mode='full'))

print("Now with float values - scipy")
print(sig.convolve(a.get() * 1.0, b.get() * 1.0, mode='full'))

print("Now with float values - scipy")
print(csig.convolve(a * 1.0, b * 1.0, mode='full'))
```

Outputs at my machine:

```
This works - scipy
[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
This works as well - cupy
[0 1 2 3 4 5 6 7 8 9]
Now with float values - scipy
[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
Now with float values - scipy
Traceback (most recent call last):
  File "/home/franz/homeofffice/homeoffice/master/fecsim/testcupy.py", line 19, in <module>
    print(csig.convolve(a * 1.0, b * 1.0, mode='full'))
  File "/home/franz/homeofffice/homeoffice/master/fecsim/.venv/lib/python3.9/site-packages/cupyx/scipy/signal/signaltools.py", line 55, in convolve
    return _correlate(in1, in2, mode, method, True)
  File "/home/franz/homeofffice/homeoffice/master/fecsim/.venv/lib/python3.9/site-packages/cupyx/scipy/signal/signaltools.py", line 125, in _correlate
    out = fftconvolve(in1, in2, mode)
  File "/home/franz/homeofffice/homeoffice/master/fecsim/.venv/lib/python3.9/site-packages/cupyx/scipy/signal/signaltools.py", line 180, in fftconvolve
    out = _st_core._freq_domain_conv(in1, in2, axes, shape, calc_fast_len=True)
  File "/home/franz/homeofffice/homeoffice/master/fecsim/.venv/lib/python3.9/site-packages/cupyx/scipy/signal/_signaltools_core.py", line 148, in _freq_domain_conv
    sp1 = fftn(in1, fshape, axes=axes)
  File "/home/franz/homeofffice/homeoffice/master/fecsim/.venv/lib/python3.9/site-packages/cupyx/scipy/fft/_fft.py", line 465, in rfftn
    return func(x, s, axes, norm, cufft.CUFFT_FORWARD, 'R2C',
  File "/home/franz/homeofffice/homeoffice/master/fecsim/.venv/lib/python3.9/site-packages/cupy/fft/_fft.py", line 229, in _fft
    raise IndexError('list index out of range')
IndexError: list index out of range
```
Before anyone is asking - I know there's a more obvious way to get the result, but the length of the second array is configurable so 1 is possible too.


Discussion:
A workaround seems `method='direct'`.

```
>>> csig.choose_conv_method(a, b)
'direct'
>>> csig.choose_conv_method(a * 1.0, b)
'fft'
>>> print(csig.convolve(a * 1.0, b * 1.0, mode='full', method='direct'))
[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
```


Resolving patch:
diff --git a/cupyx/scipy/signal/_signaltools_core.py b/cupyx/scipy/signal/_signaltools_core.py
--- a/cupyx/scipy/signal/_signaltools_core.py
+++ b/cupyx/scipy/signal/_signaltools_core.py
@@ -139,6 +139,10 @@ def _init_nd_and_axes(x, axes):
 
 def _freq_domain_conv(in1, in2, axes, shape, calc_fast_len=False):
     # See scipy's documentation in scipy.signal.signaltools
+    if not axes:
+        # rfftn/irfftn require an axis or more.
+        return in1 * in2
+
     real = (in1.dtype.kind != 'c' and in2.dtype.kind != 'c')
     fshape = ([fft.next_fast_len(shape[a], real) for a in axes]
               if calc_fast_len else shape)
diff --git a/cupyx/scipy/signal/signaltools.py b/cupyx/scipy/signal/signaltools.py
--- a/cupyx/scipy/signal/signaltools.py
+++ b/cupyx/scipy/signal/signaltools.py
@@ -117,18 +117,16 @@ def _correlate(in1, in2, mode='full', method='auto', convolution=False):
                                           convolution)
 
     # if method == 'fft':
+    if not convolution:
+        in2 = _st_core._reverse(in2).conj()
     inputs_swapped = _st_core._inputs_swap_needed(mode, in1.shape, in2.shape)
     if inputs_swapped:
         in1, in2 = in2, in1
-    if not convolution:
-        in2 = _st_core._reverse(in2).conj()
     out = fftconvolve(in1, in2, mode)
     result_type = cupy.result_type(in1, in2)
     if result_type.kind in 'ui':
         out = out.round()
     out = out.astype(result_type, copy=False)
-    if not convolution and inputs_swapped:
-        out = cupy.ascontiguousarray(_st_core._reverse(out).conj())
     return out
 
 
@@ -187,11 +185,11 @@ def choose_conv_method(in1, in2, mode='full'):
     Args:
         in1 (cupy.ndarray): first input.
         in2 (cupy.ndarray): second input.
-        mode (str, optional): ``valid``, ``same``, ``full``.
+        mode (str, optional): ``'valid'``, ``'same'``, ``'full'``.
 
     Returns:
         str: A string indicating which convolution method is fastest,
-        either ``direct`` or ``fft1``.
+        either ``'direct'`` or ``'fft'``.
 
     .. warning::
         This function currently doesn't support measure option,


Test patch:
diff --git a/tests/cupyx_tests/scipy_tests/signal_tests/test_signaltools.py b/tests/cupyx_tests/scipy_tests/signal_tests/test_signaltools.py
--- a/tests/cupyx_tests/scipy_tests/signal_tests/test_signaltools.py
+++ b/tests/cupyx_tests/scipy_tests/signal_tests/test_signaltools.py
@@ -1,9 +1,8 @@
 import sys
 import unittest
 
-import pytest
-
 import numpy as np
+import pytest
 
 import cupy
 from cupy.cuda import runtime
@@ -93,6 +92,61 @@ def test_correlate_fft(self, xp, scp, dtype):
         return self._filter('correlate', dtype, xp, scp, method='fft')
 
 
+def tupleid(shape):
+    return ''.join(str(s) for s in shape)
+
+
+@testing.with_requires('scipy')
+class TestFFTConvolveFastShape:
+    @pytest.mark.parametrize('mode', ['full', 'same', 'valid'])
+    @pytest.mark.parametrize(('shape1', 'shape2'), [
+        ((1,), (7,)),
+        ((3,), (1,)),
+        ((5, 4), (3, 1)),
+        ((1, 1), (2, 4)),
+        ((), ()),
+        ((5, 1, 1), (1, 4, 1)),
+    ], ids=tupleid)
+    @testing.for_dtypes('efdFD')
+    @testing.numpy_cupy_allclose(atol=1e-3, rtol=1e-3, scipy_name='scp')
+    def test_fftconvolve1(self, xp, scp, dtype, shape1, shape2, mode):
+        in1 = testing.shaped_random(shape1, xp, dtype)
+        in2 = testing.shaped_random(shape2, xp, dtype)
+        return scp.signal.fftconvolve(in1, in2, mode=mode)
+
+    # SciPy says "For 'valid' mode, one must be at least as large as the
+    # other in every dimension". So 'valid' is excluded from the testcases,
+    # while SciPy fails to reject them.
+    @pytest.mark.parametrize('mode', ['full', 'same'])
+    @pytest.mark.parametrize(('shape1', 'shape2'), [
+        ((5, 1), (1, 4)),
+        ((5, 1, 1), (1, 4, 1)),
+    ], ids=tupleid)
+    @testing.for_dtypes('efdFD')
+    @testing.numpy_cupy_allclose(atol=1e-3, rtol=1e-3, scipy_name='scp')
+    def test_fftconvolve1_incomparable_shape(
+            self, xp, scp, dtype, shape1, shape2, mode):
+        in1 = testing.shaped_random(shape1, xp, dtype)
+        in2 = testing.shaped_random(shape2, xp, dtype)
+        return scp.signal.fftconvolve(in1, in2, mode=mode)
+
+    @pytest.mark.parametrize('mode', ['full', 'same', 'valid'])
+    @pytest.mark.parametrize(('shape1', 'shape2', 'axes'), [
+        ((1, 4), (2, 4), (0,)),
+        # ((3, 3), (3, 3), ()), => ValueError. Only reduced axes can be empty.
+        ((2, 5, 5), (2, 1, 3), (1, 2)),
+        ((2, 5, 5), (2, 1, 1), (1, 2)),
+        ((1, 5, 5), (2, 1, 3), (1, 2)),  # broadcast
+    ], ids=tupleid)
+    @testing.for_dtypes('efdFD')
+    @testing.numpy_cupy_allclose(atol=1e-3, rtol=1e-3, scipy_name='scp')
+    def test_fftconvolve1_axes(
+            self, xp, scp, dtype, shape1, shape2, axes, mode):
+        in1 = testing.shaped_random(shape1, xp, dtype)
+        in2 = testing.shaped_random(shape2, xp, dtype)
+        return scp.signal.fftconvolve(in1, in2, mode=mode, axes=axes)
+
+
 @testing.parameterize(*testing.product({
     'size1': [(10,), (5, 10), (10, 3), (3, 10, 15)],
     'size2': [3, 4, 5, 10, None],