Repository: scipy/scipy

Issue:
fftconvolve "valid" mode checks non-convolution axes
Like `scipy.signal.convolve`, `scipy.signal.fftconvolve` has a "valid" mode, which requires all the convolution axes of one input be at least as large as all the corresponding axes of the other input.  However, if the `axes` argument is used with `mode="valid"`, `fftconvolve` does this check on all axes, rather than just the axes used for convolution.  

#### Reproducing code example:
```Python
import numpy as np
from scipy.signal import fftconvolve

in1 = np.array([[1, 1, 1, 1, 1]])
in2 = np.array([[1, 1, 1],
                [1, 1, 1]])

res = fftconvolve(in1, in2, mode='valid', axes=1)
```

#### Error message:
```
ValueError                                Traceback (most recent call last)
<ipython-input-42-5ec1639deeda> in <module>
----> 1 res = fftconvolve(in1, in2, mode='valid', axes=1)

/usr/lib64/python3.7/site-packages/scipy/signal/signaltools.py in fftconvolve(in1, in2, mode, axes)
    400 
    401     # Check that input sizes are compatible with 'valid' mode
--> 402     if _inputs_swap_needed(mode, s1, s2):
    403         # Convolution is commutative; order doesn't have any effect on output
    404         in1, s1, in2, s2 = in2, s2, in1, s1

/usr/lib64/python3.7/site-packages/scipy/signal/signaltools.py in _inputs_swap_needed(mode, shape1, shape2)
     95 
     96         if not (ok1 or ok2):
---> 97             raise ValueError("For 'valid' mode, one must be at least "
     98                              "as large as the other in every dimension")
     99 

ValueError: For 'valid' mode, one must be at least as large as the other in every dimension
```

#### Scipy/Numpy/Python version information:
```
1.3.1 1.17.1 sys.version_info(major=3, minor=7, micro=3, releaselevel='final', serial=0)
```


Discussion:

Resolving patch:
diff --git a/benchmarks/benchmarks/signal.py b/benchmarks/benchmarks/signal.py
--- a/benchmarks/benchmarks/signal.py
+++ b/benchmarks/benchmarks/signal.py
@@ -106,6 +106,25 @@ def time_convolve2d(self, mode, size):
         signal.fftconvolve(self.a, self.b, mode=mode)
 
 
+class OAConvolve(Benchmark):
+    param_names = ['mode', 'size']
+    params = [
+        ['full', 'valid', 'same'],
+        [(a, b) for a, b in product((40, 200, 3000), repeat=2)
+         if b < a]
+    ]
+
+    def setup(self, mode, size):
+        np.random.seed(1234)
+        # sample a bunch of pairs of 2d arrays
+        pairs = []
+        self.a = np.random.randn(size[0])
+        self.b = np.random.randn(size[1])
+
+    def time_convolve2d(self, mode, size):
+        signal.oaconvolve(self.a, self.b, mode=mode)
+
+
 class Convolve(Benchmark):
     param_names = ['mode']
     params = [
diff --git a/scipy/signal/__init__.py b/scipy/signal/__init__.py
--- a/scipy/signal/__init__.py
+++ b/scipy/signal/__init__.py
@@ -12,6 +12,7 @@
    convolve           -- N-dimensional convolution.
    correlate          -- N-dimensional correlation.
    fftconvolve        -- N-dimensional convolution using the FFT.
+   oaconvolve         -- N-dimensional convolution using the overlap-add method.
    convolve2d         -- 2-dimensional convolution (more options).
    correlate2d        -- 2-dimensional correlation (more options).
    sepfir2d           -- Convolve with a 2-D separable FIR filter.
diff --git a/scipy/signal/signaltools.py b/scipy/signal/signaltools.py
--- a/scipy/signal/signaltools.py
+++ b/scipy/signal/signaltools.py
@@ -21,7 +21,7 @@
                    zeros_like)
 import numpy as np
 import math
-from scipy.special import factorial
+from scipy.special import factorial, lambertw
 from .windows import get_window
 from ._arraytools import axis_slice, axis_reverse, odd_ext, even_ext, const_ext
 from .filter_design import cheby1, _validate_sos
@@ -33,7 +33,8 @@
     from fractions import gcd
 
 
-__all__ = ['correlate', 'fftconvolve', 'convolve', 'convolve2d', 'correlate2d',
+__all__ = ['correlate', 'correlate2d',
+           'convolve', 'convolve2d', 'fftconvolve', 'oaconvolve',
            'order_filter', 'medfilt', 'medfilt2d', 'wiener', 'lfilter',
            'lfiltic', 'sosfilt', 'deconvolve', 'hilbert', 'hilbert2',
            'cmplx_sort', 'unique_roots', 'invres', 'invresz', 'residue',
@@ -64,11 +65,12 @@ def _bvalfromboundary(boundary):
                          "(or 'wrap'), and 'symmetric' (or 'symm').")
 
 
-def _inputs_swap_needed(mode, shape1, shape2):
-    """
-    If in 'valid' mode, returns whether or not the input arrays need to be
+def _inputs_swap_needed(mode, shape1, shape2, axes=None):
+    """Determine if inputs arrays need to be swapped in `"valid"` mode.
+
+    If in `"valid"` mode, returns whether or not the input arrays need to be
     swapped depending on whether `shape1` is at least as large as `shape2` in
-    every dimension.
+    every calculated dimension.
 
     This is important for some of the correlation and convolution
     implementations in this module, where the larger array input needs to come
@@ -76,23 +78,25 @@ def _inputs_swap_needed(mode, shape1, shape2):
 
     Note that if the mode provided is not 'valid', False is immediately
     returned.
+
     """
-    if mode == 'valid':
-        ok1, ok2 = True, True
+    if mode != 'valid':
+        return False
 
-        for d1, d2 in zip(shape1, shape2):
-            if not d1 >= d2:
-                ok1 = False
-            if not d2 >= d1:
-                ok2 = False
+    if not shape1:
+        return False
+
+    if axes is None:
+        axes = range(len(shape1))
 
-        if not (ok1 or ok2):
-            raise ValueError("For 'valid' mode, one must be at least "
-                             "as large as the other in every dimension")
+    ok1 = all(shape1[i] >= shape2[i] for i in axes)
+    ok2 = all(shape2[i] >= shape1[i] for i in axes)
 
-        return not ok1
+    if not (ok1 or ok2):
+        raise ValueError("For 'valid' mode, one must be at least "
+                         "as large as the other in every dimension")
 
-    return False
+    return not ok1
 
 
 def _reshape_nd(x1d, ndim, axis):
@@ -281,6 +285,165 @@ def _centered(arr, newshape):
     return arr[tuple(myslice)]
 
 
+def _init_freq_conv_axes(in1, in2, mode, axes, sorted_axes=False):
+    """Handle the axes argument for frequency-domain convolution.
+
+    Returns the inputs and axes in a standard form, eliminating redundant axes,
+    swapping the inputs if necessary, and checking for various potential
+    errors.
+
+    Parameters
+    ----------
+    in1 : array
+        First input.
+    in2 : array
+        Second input.
+    mode : str {'full', 'valid', 'same'}, optional
+        A string indicating the size of the output.
+        See the documentation `fftconvolve` for more information.
+    axes : list of ints
+        Axes over which to compute the FFTs.
+    sorted_axes : bool, optional
+        If `True`, sort the axes.
+        Default is `False`, do not sort.
+
+    Returns
+    -------
+    in1 : array
+        The first input, possible swapped with the second input.
+    in2 : array
+        The second input, possible swapped with the first input.
+    axes : list of ints
+        Axes over which to compute the FFTs.
+
+    """
+    s1 = in1.shape
+    s2 = in2.shape
+    noaxes = axes is None
+
+    _, axes = _init_nd_shape_and_axes(in1, shape=None, axes=axes)
+
+    if not noaxes and not len(axes):
+        raise ValueError("when provided, axes cannot be empty")
+
+    # Axes of length 1 can rely on broadcasting rules for multipy,
+    # no fft needed.
+    axes = [a for a in axes if s1[a] != 1 and s2[a] != 1]
+
+    if sorted_axes:
+        axes.sort()
+
+    if not all(s1[a] == s2[a] or s1[a] == 1 or s2[a] == 1
+               for a in range(in1.ndim) if a not in axes):
+        raise ValueError("incompatible shapes for in1 and in2:"
+                         " {0} and {1}".format(s1, s2))
+
+    # Check that input sizes are compatible with 'valid' mode.
+    if _inputs_swap_needed(mode, s1, s2, axes=axes):
+        # Convolution is commutative; order doesn't have any effect on output.
+        in1, in2 = in2, in1
+
+    return in1, in2, axes
+
+
+def _freq_domain_conv(in1, in2, axes, shape, calc_fast_len=False):
+    """Convolve two arrays in the frequency domain.
+
+    This function implements only base the FFT-related operations.
+    Specifically, it converts the signals to the frequency domain, multiplies
+    them, then converts them back to the time domain.  Calculations of axes,
+    shapes, convolution mode, etc. are implemented in higher level-functions,
+    such as `fftconvolve` and `oaconvolve`.  Those functions should be used
+    instead of this one.
+
+    Parameters
+    ----------
+    in1 : array_like
+        First input.
+    in2 : array_like
+        Second input. Should have the same number of dimensions as `in1`.
+    axes : array_like of ints
+        Axes over which to compute the FFTs.
+    shape : array_like of ints
+        The sizes of the FFTs.
+    calc_fast_len : bool, optional
+        If `True`, set each value of `shape` to the next fast FFT length.
+        Default is `False`, use `axes` as-is.
+
+    Returns
+    -------
+    out : array
+        An N-dimensional array containing the discrete linear convolution of
+        `in1` with `in2`.
+
+    """
+    if not len(axes):
+        return in1 * in2
+
+    complex_result = (in1.dtype.kind == 'c' or in2.dtype.kind == 'c')
+
+    if calc_fast_len:
+        # Speed up FFT by padding to optimal size.
+        fshape = [
+            sp_fft.next_fast_len(shape[a], not complex_result) for a in axes]
+    else:
+        fshape = shape
+
+    if not complex_result:
+        fft, ifft = sp_fft.rfftn, sp_fft.irfftn
+    else:
+        fft, ifft = sp_fft.fftn, sp_fft.ifftn
+
+    sp1 = fft(in1, fshape, axes=axes)
+    sp2 = fft(in2, fshape, axes=axes)
+
+    ret = ifft(sp1 * sp2, fshape, axes=axes)
+
+    if calc_fast_len:
+        fslice = tuple([slice(sz) for sz in shape])
+        ret = ret[fslice]
+
+    return ret
+
+
+def _apply_conv_mode(ret, s1, s2, mode, axes):
+    """Calculate the convolution result shape based on the `mode` argument.
+
+    Returns the result sliced to the correct size for the given mode.
+
+    Parameters
+    ----------
+    ret : array
+        The result array, with the appropriate shape for the 'full' mode.
+    s1 : list of int
+        The shape of the first input.
+    s2 : list of int
+        The shape of the second input.
+    mode : str {'full', 'valid', 'same'}
+        A string indicating the size of the output.
+        See the documentation `fftconvolve` for more information.
+    axes : list of ints
+        Axes over which to compute the convolution.
+
+    Returns
+    -------
+    ret : array
+        A copy of `res`, sliced to the correct size for the given `mode`.
+
+    """
+    if mode == "full":
+        return ret.copy()
+    elif mode == "same":
+        return _centered(ret, s1).copy()
+    elif mode == "valid":
+        shape_valid = [ret.shape[a] if a not in axes else s1[a] - s2[a] + 1
+                       for a in range(ret.ndim)]
+        return _centered(ret, shape_valid).copy()
+    else:
+        raise ValueError("acceptable mode flags are 'valid',"
+                         " 'same', or 'full'")
+
+
 def fftconvolve(in1, in2, mode="full", axes=None):
     """Convolve two N-dimensional arrays using FFT.
 
@@ -313,18 +476,24 @@ def fftconvolve(in1, in2, mode="full", axes=None):
         ``same``
            The output is the same size as `in1`, centered
            with respect to the 'full' output.
-           axis : tuple, optional
     axes : int or array_like of ints or None, optional
         Axes over which to compute the convolution.
         The default is over all axes.
 
-
     Returns
     -------
     out : array
         An N-dimensional array containing a subset of the discrete linear
         convolution of `in1` with `in2`.
 
+    See Also
+    --------
+    convolve : Uses the direct convolution or FFT convolution algorithm
+               depending on which is faster.
+    oaconvolve : Uses the overlap-add method to do convolution, which is
+                 generally faster when the input arrays are large and
+                 significantly different in size.
+
     Examples
     --------
     Autocorrelation of white noise is an impulse.
@@ -368,7 +537,6 @@ def fftconvolve(in1, in2, mode="full", axes=None):
     """
     in1 = asarray(in1)
     in2 = asarray(in2)
-    noaxes = axes is None
 
     if in1.ndim == in2.ndim == 0:  # scalar inputs
         return in1 * in2
@@ -377,58 +545,334 @@ def fftconvolve(in1, in2, mode="full", axes=None):
     elif in1.size == 0 or in2.size == 0:  # empty arrays
         return array([])
 
+    in1, in2, axes = _init_freq_conv_axes(in1, in2, mode, axes,
+                                          sorted_axes=False)
+
     s1 = in1.shape
     s2 = in2.shape
-    _, axes = _init_nd_shape_and_axes(in1, shape=None, axes=axes)
 
-    if not noaxes and not len(axes):
-        raise ValueError("when provided, axes cannot be empty")
+    shape = [max((s1[i], s2[i])) if i not in axes else s1[i] + s2[i] - 1
+             for i in range(in1.ndim)]
 
-    # Axes of length 1 can rely on broadcasting rules for multipy, no fft needed
-    axes = [a for a in axes if in1.shape[a] != 1 and in2.shape[a] != 1]
+    ret = _freq_domain_conv(in1, in2, axes, shape, calc_fast_len=True)
 
-    if not all(s1[a] == s2[a] or s1[a] == 1 or s2[a] == 1
-               for a in range(in1.ndim) if a not in axes):
-        raise ValueError("incompatible shapes for in1 and in2:"
-                         " {0} and {1}".format(in1.shape, in2.shape))
+    return _apply_conv_mode(ret, s1, s2, mode, axes)
 
-    shape = [max((s1[i], s2[i])) if i not in axes else s1[i] + s2[i] - 1
-             for i in range(in1.ndim)]
 
-    complex_result = (in1.dtype.kind == 'c' or in2.dtype.kind == 'c')
+def _calc_oa_lens(s1, s2):
+    """Calculate the optimal FFT lengths for overlapp-add convolution.
 
-    # Check that input sizes are compatible with 'valid' mode
-    if _inputs_swap_needed(mode, s1, s2):
-        # Convolution is commutative; order doesn't have any effect on output
-        in1, s1, in2, s2 = in2, s2, in1, s1
+    The calculation is done for a single dimension.
 
-    if len(axes):
-        if not complex_result:
-            fft, ifft = sp_fft.rfftn, sp_fft.irfftn
-        else:
-            fft, ifft = sp_fft.fftn, sp_fft.ifftn
+    Parameters
+    ----------
+    s1 : int
+        Size of the dimension for the first array.
+    s2 : int
+        Size of the dimension for the second array.
 
-        # Speed up FFT by padding to optimal size
-        fshape = [
-            sp_fft.next_fast_len(shape[a], not complex_result) for a in axes]
-        fslice = tuple([slice(sz) for sz in shape])
-        sp1 = fft(in1, fshape, axes=axes)
-        sp2 = fft(in2, fshape, axes=axes)
-        ret = ifft(sp1 * sp2, fshape, axes=axes)[fslice].copy()
+    Returns
+    -------
+    block_size : int
+        The size of the FFT blocks.
+    overlap : int
+        The amount of overlap between two blocks.
+    in1_step : int
+        The size of each step for the first array.
+    in2_step : int
+        The size of each step for the first array.
+
+    """
+    # Set up the arguments for the conventional FFT approach.
+    fallback = (s1+s2-1, None, s1, s2)
+
+    # Use conventional FFT convolve if sizes are same.
+    if s1 == s2 or s1 == 1 or s2 == 1:
+        return fallback
+
+    if s2 > s1:
+        s1, s2 = s2, s1
+        swapped = True
     else:
-        ret = in1 * in2
+        swapped = False
 
-    if mode == "full":
-        return ret
-    elif mode == "same":
-        return _centered(ret, s1)
-    elif mode == "valid":
-        shape_valid = [shape[a] if a not in axes else s1[a] - s2[a] + 1
-                       for a in range(in1.ndim)]
-        return _centered(ret, shape_valid)
+    # There cannot be a useful block size if s2 is more than half of s1.
+    if s2 >= s1/2:
+        return fallback
+
+    # Derivation of optimal block length
+    # For original formula see:
+    # https://en.wikipedia.org/wiki/Overlap-add_method
+    #
+    # Formula:
+    # K = overlap = s2-1
+    # N = block_size
+    # C = complexity
+    # e = exponential, exp(1)
+    #
+    # C = (N*(log2(N)+1))/(N-K)
+    # C = (N*log2(2N))/(N-K)
+    # C = N/(N-K) * log2(2N)
+    # C1 = N/(N-K)
+    # C2 = log2(2N) = ln(2N)/ln(2)
+    #
+    # dC1/dN = (1*(N-K)-N)/(N-K)^2 = -K/(N-K)^2
+    # dC2/dN = 2/(2*N*ln(2)) = 1/(N*ln(2))
+    #
+    # dC/dN = dC1/dN*C2 + dC2/dN*C1
+    # dC/dN = -K*ln(2N)/(ln(2)*(N-K)^2) + N/(N*ln(2)*(N-K))
+    # dC/dN = -K*ln(2N)/(ln(2)*(N-K)^2) + 1/(ln(2)*(N-K))
+    # dC/dN = -K*ln(2N)/(ln(2)*(N-K)^2) + (N-K)/(ln(2)*(N-K)^2)
+    # dC/dN = (-K*ln(2N) + (N-K)/(ln(2)*(N-K)^2)
+    # dC/dN = (N - K*ln(2N) - K)/(ln(2)*(N-K)^2)
+    #
+    # Solve for minimum, where dC/dN = 0
+    # 0 = (N - K*ln(2N) - K)/(ln(2)*(N-K)^2)
+    # 0 * ln(2)*(N-K)^2 = N - K*ln(2N) - K
+    # 0 = N - K*ln(2N) - K
+    # 0 = N - K*(ln(2N) + 1)
+    # 0 = N - K*ln(2Ne)
+    # N = K*ln(2Ne)
+    # N/K = ln(2Ne)
+    #
+    # e^(N/K) = e^ln(2Ne)
+    # e^(N/K) = 2Ne
+    # 1/e^(N/K) = 1/(2*N*e)
+    # e^(N/-K) = 1/(2*N*e)
+    # e^(N/-K) = K/N*1/(2*K*e)
+    # N/K*e^(N/-K) = 1/(2*e*K)
+    # N/-K*e^(N/-K) = -1/(2*e*K)
+    #
+    # Using Lambert W function
+    # https://en.wikipedia.org/wiki/Lambert_W_function
+    # x = W(y) It is the solution to y = x*e^x
+    # x = N/-K
+    # y = -1/(2*e*K)
+    #
+    # N/-K = W(-1/(2*e*K))
+    #
+    # N = -K*W(-1/(2*e*K))
+    overlap = s2-1
+    opt_size = -overlap*lambertw(-1/(2*math.e*overlap), k=-1).real
+    block_size = sp_fft.next_fast_len(math.ceil(opt_size))
+
+    # Use conventional FFT convolve if there is only going to be one block.
+    if block_size >= s1:
+        return fallback
+
+    if not swapped:
+        in1_step = block_size-s2+1
+        in2_step = s2
     else:
-        raise ValueError("acceptable mode flags are 'valid',"
-                         " 'same', or 'full'")
+        in1_step = s2
+        in2_step = block_size-s2+1
+
+    return block_size, overlap, in1_step, in2_step
+
+
+def oaconvolve(in1, in2, mode="full", axes=None):
+    """Convolve two N-dimensional arrays using the overlap-add method.
+
+    Convolve `in1` and `in2` using the overlap-add method, with
+    the output size determined by the `mode` argument.
+
+    This is generally much faster than `convolve` for large arrays (n > ~500),
+    and generally much faster than `fftconvolve` when one array is much
+    larger than the other, but can be slower when only a few output values are
+    needed or when the arrays are very similar in shape, and can only
+    output float arrays (int or object array inputs will be cast to float).
+
+    Parameters
+    ----------
+    in1 : array_like
+        First input.
+    in2 : array_like
+        Second input. Should have the same number of dimensions as `in1`.
+    mode : str {'full', 'valid', 'same'}, optional
+        A string indicating the size of the output:
+
+        ``full``
+           The output is the full discrete linear convolution
+           of the inputs. (Default)
+        ``valid``
+           The output consists only of those elements that do not
+           rely on the zero-padding. In 'valid' mode, either `in1` or `in2`
+           must be at least as large as the other in every dimension.
+        ``same``
+           The output is the same size as `in1`, centered
+           with respect to the 'full' output.
+    axes : int or array_like of ints or None, optional
+        Axes over which to compute the convolution.
+        The default is over all axes.
+
+    Returns
+    -------
+    out : array
+        An N-dimensional array containing a subset of the discrete linear
+        convolution of `in1` with `in2`.
+
+    See Also
+    --------
+    convolve : Uses the direct convolution or FFT convolution algorithm
+               depending on which is faster.
+    fftconvolve : An implementation of convolution using FFT.
+
+    Notes
+    -----
+    .. versionadded:: 1.4.0
+
+    Examples
+    --------
+    Convolve a 100,000 sample signal with a 512-sample filter.
+
+    >>> from scipy import signal
+    >>> sig = np.random.randn(100000)
+    >>> filt = signal.firwin(512, 0.01)
+    >>> fsig = signal.oaconvolve(sig, filt)
+
+    >>> import matplotlib.pyplot as plt
+    >>> fig, (ax_orig, ax_mag) = plt.subplots(2, 1)
+    >>> ax_orig.plot(sig)
+    >>> ax_orig.set_title('White noise')
+    >>> ax_mag.plot(fsig)
+    >>> ax_mag.set_title('Filtered noise')
+    >>> fig.tight_layout()
+    >>> fig.show()
+
+    References
+    ----------
+    .. [1] Wikipedia, "Overlap-add_method".
+           https://en.wikipedia.org/wiki/Overlap-add_method
+    .. [2] Richard G. Lyons. Understanding Digital Signal Processing,
+           Third Edition, 2011. Chapter 13.10.
+           ISBN 13: 978-0137-02741-5
+
+    """
+    in1 = asarray(in1)
+    in2 = asarray(in2)
+
+    if in1.ndim == in2.ndim == 0:  # scalar inputs
+        return in1 * in2
+    elif in1.ndim != in2.ndim:
+        raise ValueError("in1 and in2 should have the same dimensionality")
+    elif in1.size == 0 or in2.size == 0:  # empty arrays
+        return array([])
+    elif in1.shape == in2.shape:  # Equivalent to fftconvolve
+        return fftconvolve(in1, in2, mode=mode, axes=axes)
+
+    in1, in2, axes = _init_freq_conv_axes(in1, in2, mode, axes,
+                                          sorted_axes=True)
+
+    if not axes:
+        return in1*in2
+
+    s1 = in1.shape
+    s2 = in2.shape
+
+    # Calculate this now since in1 is changed later
+    shape_final = [None if i not in axes else
+                   s1[i] + s2[i] - 1 for i in range(in1.ndim)]
+
+    # Calculate the block sizes for the output, steps, first and second inputs.
+    # It is simpler to calculate them all together than doing them in separate
+    # loops due to all the special cases that need to be handled.
+    optimal_sizes = ((-1, -1, s1[i], s2[i]) if i not in axes else
+                     _calc_oa_lens(s1[i], s2[i]) for i in range(in1.ndim))
+    block_size, overlaps, \
+        in1_step, in2_step = zip(*optimal_sizes)
+
+    # Fall back to fftconvolve if there is only one block in every dimension.
+    if in1_step == s1 and in2_step == s2:
+        return fftconvolve(in1, in2, mode=mode, axes=axes)
+
+    # Figure out the number of steps and padding.
+    # This would get too complicated in a list comprehension.
+    nsteps1 = []
+    nsteps2 = []
+    pad_size1 = []
+    pad_size2 = []
+    for i in range(in1.ndim):
+        if i not in axes:
+            pad_size1 += [(0, 0)]
+            pad_size2 += [(0, 0)]
+            continue
+
+        if s1[i] > in1_step[i]:
+            curnstep1 = math.ceil((s1[i]+1)/in1_step[i])
+            if (block_size[i] - overlaps[i])*curnstep1 < shape_final[i]:
+                curnstep1 += 1
+
+            curpad1 = curnstep1*in1_step[i] - s1[i]
+        else:
+            curnstep1 = 1
+            curpad1 = 0
+
+        if s2[i] > in2_step[i]:
+            curnstep2 = math.ceil((s2[i]+1)/in2_step[i])
+            if (block_size[i] - overlaps[i])*curnstep2 < shape_final[i]:
+                curnstep2 += 1
+
+            curpad2 = curnstep2*in2_step[i] - s2[i]
+        else:
+            curnstep2 = 1
+            curpad2 = 0
+
+        nsteps1 += [curnstep1]
+        nsteps2 += [curnstep2]
+        pad_size1 += [(0, curpad1)]
+        pad_size2 += [(0, curpad2)]
+
+    # Pad the array to a size that can be reshaped to the desired shape
+    # if necessary.
+    if not all(curpad == (0, 0) for curpad in pad_size1):
+        in1 = np.pad(in1, pad_size1, mode='constant', constant_values=0)
+
+    if not all(curpad == (0, 0) for curpad in pad_size2):
+        in2 = np.pad(in2, pad_size2, mode='constant', constant_values=0)
+
+    # Reshape the overlap-add parts to input block sizes.
+    split_axes = [iax+i for i, iax in enumerate(axes)]
+    fft_axes = [iax+1 for iax in split_axes]
+
+    # We need to put each new dimension before the corresponding dimension
+    # being reshaped in order to get the data in the right layout at the end.
+    reshape_size1 = list(in1_step)
+    reshape_size2 = list(in2_step)
+    for i, iax in enumerate(split_axes):
+        reshape_size1.insert(iax, nsteps1[i])
+        reshape_size2.insert(iax, nsteps2[i])
+
+    in1 = in1.reshape(*reshape_size1)
+    in2 = in2.reshape(*reshape_size2)
+
+    # Do the convolution.
+    fft_shape = [block_size[i] for i in axes]
+    ret = _freq_domain_conv(in1, in2, fft_axes, fft_shape, calc_fast_len=False)
+
+    # Do the overlap-add.
+    for ax, ax_fft, ax_split in zip(axes, fft_axes, split_axes):
+        overlap = overlaps[ax]
+        if overlap is None:
+            continue
+
+        ret, overpart = np.split(ret, [-overlap], ax_fft)
+        overpart = np.split(overpart, [-1], ax_split)[0]
+
+        ret_overpart = np.split(ret, [overlap], ax_fft)[0]
+        ret_overpart = np.split(ret_overpart, [1], ax_split)[1]
+        ret_overpart += overpart
+
+    # Reshape back to the correct dimensionality.
+    shape_ret = [ret.shape[i] if i not in fft_axes else
+                 ret.shape[i]*ret.shape[i-1]
+                 for i in range(ret.ndim) if i not in split_axes]
+    ret = ret.reshape(*shape_ret)
+
+    # Slice to the correct size.
+    slice_final = tuple([slice(islice) for islice in shape_final])
+    ret = ret[slice_final]
+
+    return _apply_conv_mode(ret, s1, s2, mode, axes)
 
 
 def _numeric_arrays(arrays, kinds='buifc'):
@@ -738,7 +1182,10 @@ def convolve(in1, in2, mode='full', method='auto'):
     numpy.polymul : performs polynomial multiplication (same operation, but
                     also accepts poly1d objects)
     choose_conv_method : chooses the fastest appropriate convolution method
-    fftconvolve
+    fftconvolve : Always uses the FFT method.
+    oaconvolve : Uses the overlap-add method to do convolution, which is
+                 generally faster when the input arrays are large and
+                 significantly different in size.
 
     Notes
     -----


Test patch:
diff --git a/scipy/signal/tests/test_signaltools.py b/scipy/signal/tests/test_signaltools.py
--- a/scipy/signal/tests/test_signaltools.py
+++ b/scipy/signal/tests/test_signaltools.py
@@ -21,7 +21,8 @@
 from scipy.optimize import fmin
 from scipy import signal
 from scipy.signal import (
-    correlate, convolve, convolve2d, fftconvolve, choose_conv_method,
+    correlate, convolve, convolve2d,
+    fftconvolve, oaconvolve, choose_conv_method,
     hilbert, hilbert2, lfilter, lfilter_zi, filtfilt, butter, zpk2tf, zpk2sos,
     invres, invresz, vectorstrength, lfiltic, tf2sos, sosfilt, sosfiltfilt,
     sosfilt_zi, tf2zpk, BadCoefficients, detrend)
@@ -655,6 +656,19 @@ def test_valid_mode_complex_axes(self, axes):
         out = fftconvolve(b, a, 'valid', axes=axes)
         assert_array_almost_equal(out, expected)
 
+    def test_valid_mode_ignore_nonaxes(self):
+        # See gh-5897
+        a = array([3, 2, 1])
+        b = array([3, 3, 5, 6, 8, 7, 9, 0, 1])
+        expected = array([24., 31., 41., 43., 49., 25., 12.])
+
+        a = np.tile(a, [2, 1])
+        b = np.tile(b, [1, 1])
+        expected = np.tile(expected, [2, 1])
+
+        out = fftconvolve(a, b, 'valid', axes=1)
+        assert_array_almost_equal(out, expected)
+
     def test_empty(self):
         # Regression test for #1745: crashes with 0-length input.
         assert_(fftconvolve([], []).size == 0)
@@ -748,62 +762,236 @@ def test_many_sizes(self, n):
         out = fftconvolve(a, b, 'full', axes=[0])
         assert_allclose(out, expected, atol=1e-10)
 
-    def test_invalid_shapes(self):
+
+def fftconvolve_err(*args, **kwargs):
+    raise RuntimeError('Fell back to fftconvolve')
+
+
+def gen_oa_shapes(sizes):
+    return [(a, b) for a, b in product(sizes, repeat=2)
+            if abs(a - b) > 3]
+
+
+def gen_oa_shapes_2d(sizes):
+    shapes0 = gen_oa_shapes(sizes)
+    shapes1 = gen_oa_shapes(sizes)
+    shapes = [ishapes0+ishapes1 for ishapes0, ishapes1 in
+              zip(shapes0, shapes1)]
+
+    modes = ['full', 'valid', 'same']
+    return [ishapes+(imode,) for ishapes, imode in product(shapes, modes)
+            if imode != 'valid' or
+            (ishapes[0] > ishapes[1] and ishapes[2] > ishapes[3]) or
+            (ishapes[0] < ishapes[1] and ishapes[2] < ishapes[3])]
+
+
+def gen_oa_shapes_eq(sizes):
+    return [(a, b) for a, b in product(sizes, repeat=2)
+            if a >= b]
+
+
+class TestOAConvolve(object):
+    @pytest.mark.slow()
+    @pytest.mark.parametrize('shape_a_0, shape_b_0',
+                             gen_oa_shapes_eq(list(range(100)) +
+                                              list(range(100, 1000, 23)))
+                             )
+    def test_real_manylens(self, shape_a_0, shape_b_0):
+        a = np.random.rand(shape_a_0)
+        b = np.random.rand(shape_b_0)
+
+        expected = fftconvolve(a, b)
+        out = oaconvolve(a, b)
+
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('shape_a_0, shape_b_0',
+                             gen_oa_shapes([50, 47, 6, 4]))
+    @pytest.mark.parametrize('is_complex', [True, False])
+    @pytest.mark.parametrize('mode', ['full', 'valid', 'same'])
+    def test_1d_noaxes(self, shape_a_0, shape_b_0,
+                       is_complex, mode, monkeypatch):
+        a = np.random.rand(shape_a_0)
+        b = np.random.rand(shape_b_0)
+        if is_complex:
+            a = a + 1j*np.random.rand(shape_a_0)
+            b = b + 1j*np.random.rand(shape_b_0)
+
+        expected = fftconvolve(a, b, mode=mode)
+
+        monkeypatch.setattr(signal.signaltools, 'fftconvolve',
+                            fftconvolve_err)
+        out = oaconvolve(a, b, mode=mode)
+
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('axes', [0, 1])
+    @pytest.mark.parametrize('shape_a_0, shape_b_0',
+                             gen_oa_shapes([50, 47, 6, 4]))
+    @pytest.mark.parametrize('shape_a_extra', [1, 3])
+    @pytest.mark.parametrize('shape_b_extra', [1, 3])
+    @pytest.mark.parametrize('is_complex', [True, False])
+    @pytest.mark.parametrize('mode', ['full', 'valid', 'same'])
+    def test_1d_axes(self, axes, shape_a_0, shape_b_0,
+                     shape_a_extra, shape_b_extra,
+                     is_complex, mode, monkeypatch):
+        ax_a = [shape_a_extra]*2
+        ax_b = [shape_b_extra]*2
+        ax_a[axes] = shape_a_0
+        ax_b[axes] = shape_b_0
+
+        a = np.random.rand(*ax_a)
+        b = np.random.rand(*ax_b)
+        if is_complex:
+            a = a + 1j*np.random.rand(*ax_a)
+            b = b + 1j*np.random.rand(*ax_b)
+
+        expected = fftconvolve(a, b, mode=mode, axes=axes)
+
+        monkeypatch.setattr(signal.signaltools, 'fftconvolve',
+                            fftconvolve_err)
+        out = oaconvolve(a, b, mode=mode, axes=axes)
+
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('shape_a_0, shape_b_0, '
+                             'shape_a_1, shape_b_1, mode',
+                             gen_oa_shapes_2d([50, 47, 6, 4]))
+    @pytest.mark.parametrize('is_complex', [True, False])
+    def test_2d_noaxes(self, shape_a_0, shape_b_0,
+                       shape_a_1, shape_b_1, mode,
+                       is_complex, monkeypatch):
+        a = np.random.rand(shape_a_0, shape_a_1)
+        b = np.random.rand(shape_b_0, shape_b_1)
+        if is_complex:
+            a = a + 1j*np.random.rand(shape_a_0, shape_a_1)
+            b = b + 1j*np.random.rand(shape_b_0, shape_b_1)
+
+        expected = fftconvolve(a, b, mode=mode)
+
+        monkeypatch.setattr(signal.signaltools, 'fftconvolve',
+                            fftconvolve_err)
+        out = oaconvolve(a, b, mode=mode)
+
+        assert_array_almost_equal(out, expected)
+
+    @pytest.mark.parametrize('axes', [[0, 1], [0, 2], [1, 2]])
+    @pytest.mark.parametrize('shape_a_0, shape_b_0, '
+                             'shape_a_1, shape_b_1, mode',
+                             gen_oa_shapes_2d([50, 47, 6, 4]))
+    @pytest.mark.parametrize('shape_a_extra', [1, 3])
+    @pytest.mark.parametrize('shape_b_extra', [1, 3])
+    @pytest.mark.parametrize('is_complex', [True, False])
+    def test_2d_axes(self, axes, shape_a_0, shape_b_0,
+                     shape_a_1, shape_b_1, mode,
+                     shape_a_extra, shape_b_extra,
+                     is_complex, monkeypatch):
+        ax_a = [shape_a_extra]*3
+        ax_b = [shape_b_extra]*3
+        ax_a[axes[0]] = shape_a_0
+        ax_b[axes[0]] = shape_b_0
+        ax_a[axes[1]] = shape_a_1
+        ax_b[axes[1]] = shape_b_1
+
+        a = np.random.rand(*ax_a)
+        b = np.random.rand(*ax_b)
+        if is_complex:
+            a = a + 1j*np.random.rand(*ax_a)
+            b = b + 1j*np.random.rand(*ax_b)
+
+        expected = fftconvolve(a, b, mode=mode, axes=axes)
+
+        monkeypatch.setattr(signal.signaltools, 'fftconvolve',
+                            fftconvolve_err)
+        out = oaconvolve(a, b, mode=mode, axes=axes)
+
+        assert_array_almost_equal(out, expected)
+
+    def test_empty(self):
+        # Regression test for #1745: crashes with 0-length input.
+        assert_(oaconvolve([], []).size == 0)
+        assert_(oaconvolve([5, 6], []).size == 0)
+        assert_(oaconvolve([], [7]).size == 0)
+
+    def test_zero_rank(self):
+        a = array(4967)
+        b = array(3920)
+        out = oaconvolve(a, b)
+        assert_equal(out, a * b)
+
+    def test_single_element(self):
+        a = array([4967])
+        b = array([3920])
+        out = oaconvolve(a, b)
+        assert_equal(out, a * b)
+
+
+class TestAllFreqConvolves(object):
+
+    @pytest.mark.parametrize('convapproach',
+                             [fftconvolve, oaconvolve])
+    def test_invalid_shapes(self, convapproach):
         a = np.arange(1, 7).reshape((2, 3))
         b = np.arange(-6, 0).reshape((3, 2))
         with assert_raises(ValueError,
                            match="For 'valid' mode, one must be at least "
                            "as large as the other in every dimension"):
-            fftconvolve(a, b, mode='valid')
+            convapproach(a, b, mode='valid')
 
-    def test_invalid_shapes_axes(self):
+    @pytest.mark.parametrize('convapproach',
+                             [fftconvolve, oaconvolve])
+    def test_invalid_shapes_axes(self, convapproach):
         a = np.zeros([5, 6, 2, 1])
         b = np.zeros([5, 6, 3, 1])
         with assert_raises(ValueError,
                            match=r"incompatible shapes for in1 and in2:"
                            r" \(5L?, 6L?, 2L?, 1L?\) and"
                            r" \(5L?, 6L?, 3L?, 1L?\)"):
-            fftconvolve(a, b, axes=[0, 1])
+            convapproach(a, b, axes=[0, 1])
 
     @pytest.mark.parametrize('a,b',
                              [([1], 2),
                               (1, [2]),
                               ([3], [[2]])])
-    def test_mismatched_dims(self, a, b):
+    @pytest.mark.parametrize('convapproach',
+                             [fftconvolve, oaconvolve])
+    def test_mismatched_dims(self, a, b, convapproach):
         with assert_raises(ValueError,
                            match="in1 and in2 should have the same"
                            " dimensionality"):
-            fftconvolve(a, b)
+            convapproach(a, b)
 
-    def test_invalid_flags(self):
+    @pytest.mark.parametrize('convapproach',
+                             [fftconvolve, oaconvolve])
+    def test_invalid_flags(self, convapproach):
         with assert_raises(ValueError,
                            match="acceptable mode flags are 'valid',"
                            " 'same', or 'full'"):
-            fftconvolve([1], [2], mode='chips')
+            convapproach([1], [2], mode='chips')
 
         with assert_raises(ValueError,
                            match="when provided, axes cannot be empty"):
-            fftconvolve([1], [2], axes=[])
+            convapproach([1], [2], axes=[])
 
         with assert_raises(ValueError, match="axes must be a scalar or "
                            "iterable of integers"):
-            fftconvolve([1], [2], axes=[[1, 2], [3, 4]])
+            convapproach([1], [2], axes=[[1, 2], [3, 4]])
 
         with assert_raises(ValueError, match="axes must be a scalar or "
                            "iterable of integers"):
-            fftconvolve([1], [2], axes=[1., 2., 3., 4.])
+            convapproach([1], [2], axes=[1., 2., 3., 4.])
 
         with assert_raises(ValueError,
                            match="axes exceeds dimensionality of input"):
-            fftconvolve([1], [2], axes=[1])
+            convapproach([1], [2], axes=[1])
 
         with assert_raises(ValueError,
                            match="axes exceeds dimensionality of input"):
-            fftconvolve([1], [2], axes=[-2])
+            convapproach([1], [2], axes=[-2])
 
         with assert_raises(ValueError,
                            match="all axes must be unique"):
-            fftconvolve([1], [2], axes=[0, 0])
+            convapproach([1], [2], axes=[0, 0])
 
 
 class TestMedFilt(object):