Repository: scipy/scipy

Issue:
Resampling using polyphase filtering
Now that upfirdn polyphase filtering is implemented, it should be possible to do upfirdn-based resampling. Hopefully this will be faster than the current FFT-based method.

I don't actually know the correct way to do upfirdn-based resampling, but it looks like Octave does [something reasonable](http://octave.sourceforge.net/signal/function/resample.html) that is similar to [what MATLAB does](http://www.mathworks.com/help/signal/ref/resample.html). Although I can't work from the Octave's GPL code, I think I could read the book chapter / equations referenced in the GPL code, and work from that:

http://sourceforge.net/p/octave/signal/ci/default/tree/inst/resample.m

Does that sound about right from a licensing standpoint @WarrenWeckesser @rgommers?

@sirtom67 do you have any other starting points (code or suggesting reading) other than Oppenheim and Schafer (Discrete-time signal processing)? I think I have a copy of that book kicking around somewhere...



Discussion:
The matlab resample command just does the following:
1) design a low-pass FIR filter with the proper cutoff frequency and some
reasonable rules for setting the length to get a filter that isn't too long
2) call upfirdn
3) get rid of precursor and postcursor filtering tails to remove delay

IIRC, the matlab command used fir1 with a kaiser window. Similar
functionality exists in scipy.signal.

On Wed, Jan 20, 2016 at 12:43 PM, Eric Larson notifications@github.com
wrote:

> Now that upfirdn polyphase filtering is implemented, it should be possible
> to do upfirdn-based resampling. Hopefully this will be faster than the
> current FFT-based method.
> 
> I don't actually know the correct way to do upfirdn-based resampling, but
> it looks like Octave does something reasonable
> http://octave.sourceforge.net/signal/function/resample.html that is
> similar to what MATLAB does
> http://www.mathworks.com/help/signal/ref/resample.html. Although I
> can't work from the Octave's GPL code, I think I could read the book
> chapter / equations referenced in the GPL code, and work from that:
> 
> http://sourceforge.net/p/octave/signal/ci/default/tree/inst/resample.m
> 
> Does that sound about right from a licensing standpoint @WarrenWeckesser
> https://github.com/WarrenWeckesser @rgommers
> https://github.com/rgommers?
> 
> @sirtom67 https://github.com/sirtom67 do you have any other starting
> points (code or suggesting reading) other than Oppenheim and Schafer
> (Discrete-time signal processing)? I think I have a copy of that book
> kicking around somewhere...
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/scipy/scipy/issues/5746.

Good to hear, that's what I had gathered from reading the documentation of MATLAB and Octavie. MATLAB apparently uses `firls` but I remember someone saying (mailing list somewhere?) that `firwin` or `firwin` could be used instead in `scipy`. I'll track down Oppenheim and Shafer to get the details right, then.


Resolving patch:
diff --git a/scipy/signal/__init__.py b/scipy/signal/__init__.py
--- a/scipy/signal/__init__.py
+++ b/scipy/signal/__init__.py
@@ -66,6 +66,7 @@
    decimate      -- Downsample a signal.
    detrend       -- Remove linear and/or constant trends from data.
    resample      -- Resample using Fourier method.
+   resample_poly -- Resample using polyphase filtering method.
    upfirdn       -- Upsample, apply FIR filter, downsample.
 
 Filter design
diff --git a/scipy/signal/signaltools.py b/scipy/signal/signaltools.py
--- a/scipy/signal/signaltools.py
+++ b/scipy/signal/signaltools.py
@@ -5,8 +5,10 @@
 
 import warnings
 import threading
+from fractions import gcd
 
 from . import sigtools
+from ._upfirdn import _UpFIRDn, _output_len
 from scipy._lib.six import callable
 from scipy._lib._version import NumpyVersion
 from scipy import fftpack, linalg
@@ -27,7 +29,8 @@
            'order_filter', 'medfilt', 'medfilt2d', 'wiener', 'lfilter',
            'lfiltic', 'sosfilt', 'deconvolve', 'hilbert', 'hilbert2',
            'cmplx_sort', 'unique_roots', 'invres', 'invresz', 'residue',
-           'residuez', 'resample', 'detrend', 'lfilter_zi', 'sosfilt_zi',
+           'residuez', 'resample', 'resample_poly', 'detrend',
+           'lfilter_zi', 'sosfilt_zi',
            'filtfilt', 'decimate', 'vectorstrength']
 
 
@@ -1721,6 +1724,11 @@ def resample(x, num, t=None, axis=0, window=None):
         containing the resampled array and the corresponding resampled
         positions.
 
+    See also
+    --------
+    decimate
+    resample_poly
+
     Notes
     -----
     The argument `window` controls a Fourier-domain window that tapers
@@ -1804,6 +1812,122 @@ def resample(x, num, t=None, axis=0, window=None):
         return y, new_t
 
 
+def resample_poly(x, up, down, axis=0, window=('kaiser', 5.0)):
+    """
+    Resample `x` along the given axis using polyphase filtering.
+
+    The signal `x` is upsampled by the factor `up`, a zero-phase low-pass
+    FIR filter is applied, and then it is downsampled by the factor `down`.
+    The resulting sample rate is ``up / down`` times the original sample
+    rate. Values beyond the boundary of the signal are assumed to be zero
+    during the filtering step.
+
+    Parameters
+    ----------
+    x : array_like
+        The data to be resampled.
+    up : int
+        The upsampling factor.
+    down : int
+        The downsampling factor.
+    axis : int, optional
+        The axis of `x` that is resampled.  Default is 0.
+    window : string or tuple of string and parameter values
+        Desired window to use to design the low-pass filter. See
+        `scipy.signal.get_window` for a list of windows and required
+        parameters.
+
+    Returns
+    -------
+    resampled_x : array
+        The resampled array.
+
+    See also
+    --------
+    decimate
+    resample
+
+    Notes
+    -----
+    This polyphase method will likely be faster than the Fourier method
+    in `scipy.signal.resample` when the number of samples is large and
+    prime, or when the number of samples is large and `up` and `down`
+    share a large greatest common denominator. The length of the FIR
+    filter used will depend on ``max(up, down) // gcd(up, down)``, and
+    the number of operations during polyphase filtering will depend on
+    the filter length and `down` (see `scipy.signal.upfirdn` for details).
+
+    The `window` argument is passed directly to `scipy.signal.firwin`
+    to design a low-pass filter.
+
+    The first sample of the returned vector is the same as the first
+    sample of the input vector.  The spacing between samples is changed
+    from ``dx`` to ``dx * up / float(down)``.
+
+    Examples
+    --------
+    Note that the end of the resampled data rises to meet the first
+    sample of the next cycle for the FFT method, and gets closer to zero
+    for the polyphase method:
+
+    >>> from scipy import signal
+    
+    >>> x = np.linspace(0, 10, 20, endpoint=False)
+    >>> y = np.cos(-x**2/6.0)
+    >>> f_fft = signal.resample(y, 100)
+    >>> f_poly = signal.resample_poly(y, 100, 20)
+    >>> xnew = np.linspace(0, 10, 100, endpoint=False)
+    
+    >>> import matplotlib.pyplot as plt
+    >>> plt.plot(xnew, f_fft, 'b.-', xnew, f_poly, 'r.-')
+    >>> plt.plot(x, y, 'ko-')
+    >>> plt.plot(10, y[0], 'bo', 10, 0., 'ro')  # boundaries
+    >>> plt.legend(['resample', 'resamp_poly', 'data'], loc='best')
+    >>> plt.show()
+    """
+    x = asarray(x)
+    up = int(up)
+    down = int(down)
+    if up < 1 or down < 1:
+        raise ValueError('up and down must be >= 1')
+
+    # Determine our up and down factors
+    # Use a rational approimation to save computation time on really long
+    # signals
+    g_ = gcd(up, down)
+    up //= g_
+    down //= g_
+    if up == down == 1:
+        return x.copy()
+    n_out = (x.shape[axis] * up) // down
+
+    # Design a linear-phase low-pass FIR filter
+    max_rate = max(up, down)
+    f_c = 1. / max_rate  # cutoff of FIR filter (rel. to Nyquist)
+    half_len = 10 * max_rate  # reasonable cutoff for our sinc-like function
+    h = firwin(2 * half_len + 1, f_c, window=window)
+    h *= up
+
+    # Zero-pad our filter to put the output samples at the center
+    n_pre_pad = (down - half_len % down)
+    n_post_pad = 0
+    n_pre_remove = (half_len + n_pre_pad) // down
+    # We should rarely need to do this given our filter lengths...
+    while _output_len(len(h) + n_pre_pad + n_post_pad, x.shape[axis],
+                      up, down) < n_out + n_pre_remove:
+        n_post_pad += 1
+    h = np.concatenate((np.zeros(n_pre_pad), h, np.zeros(n_post_pad)))
+    ufd = _UpFIRDn(h, x.dtype, up, down)
+    n_pre_remove_end = n_pre_remove + n_out
+
+    def apply_remove(x):
+        """Apply the upfirdn filter and remove excess"""
+        return ufd.apply_filter(x)[n_pre_remove:n_pre_remove_end]
+
+    y = np.apply_along_axis(apply_remove, axis, x)
+    return y
+
+
 def vectorstrength(events, period):
     '''
     Determine the vector strength of the events corresponding to the given
@@ -2709,10 +2833,10 @@ def decimate(x, q, n=None, ftype='iir', axis=-1):
     y : ndarray
         The down-sampled signal.
 
-    See also
+    See Also
     --------
     resample
-
+    resample_poly
     """
 
     if not isinstance(q, int):


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
@@ -476,6 +476,8 @@ def test_basic(self):
 class TestResample(TestCase):
 
     def test_basic(self):
+        # Some basic tests
+
         # Regression test for issue #3603.
         # window.shape must equal to sig.shape[0]
         sig = np.arange(128)
@@ -483,6 +485,60 @@ def test_basic(self):
         win = signal.get_window(('kaiser', 8.0), 160)
         assert_raises(ValueError, signal.resample, sig, num, window=win)
 
+        # Other degenerate conditions
+        assert_raises(ValueError, signal.resample_poly, sig, 'yo', 1)
+        assert_raises(ValueError, signal.resample_poly, sig, 1, 0)
+
+    def test_fft(self):
+        # Test FFT-based resampling
+        self._test_data(method='fft')
+
+    def test_polyphase(self):
+        # Test polyphase resampling
+        self._test_data(method='polyphase')
+
+    def _test_data(self, method):
+        # Test resampling of sinusoids and random noise (1-sec)
+        rate = 100
+        rates_to = [49, 50, 51, 99, 100, 101, 199, 200, 201]
+
+        # Sinusoids, windowed to avoid edge artifacts
+        t = np.arange(rate) / float(rate)
+        freqs = np.array((1., 10., 40.))[:, np.newaxis]
+        x = np.sin(2 * np.pi * freqs * t) * np.hanning(rate)
+
+        print(method)
+        for rate_to in rates_to:
+            t_to = np.arange(rate_to) / float(rate_to)
+            y_tos = np.sin(2 * np.pi * freqs * t_to) * np.hanning(rate_to)
+            if method == 'fft':
+                y_resamps = signal.resample(x, rate_to, axis=-1)
+            else:
+                y_resamps = signal.resample_poly(x, rate_to, rate, axis=-1)
+            for y_to, y_resamp, freq in zip(y_tos, y_resamps, freqs):
+                if freq >= 0.5 * rate_to:
+                    y_to.fill(0.)  # mostly low-passed away
+                    assert_allclose(y_resamp, y_to, atol=1e-3)
+                else:
+                    assert_array_equal(y_to.shape, y_resamp.shape)
+                    corr = np.corrcoef(y_to, y_resamp)[0, 1]
+                    assert_(corr > 0.99, msg=corr)
+
+        # Random data
+        rng = np.random.RandomState(0)
+        x = np.hanning(rate) * np.cumsum(rng.randn(rate))  # low-pass, wind
+        for rate_to in rates_to:
+            # random data
+            t_to = np.arange(rate_to) / float(rate_to)
+            y_to = np.interp(t_to, t, x)
+            if method == 'fft':
+                y_resamp = signal.resample(x, rate_to)
+            else:
+                y_resamp = signal.resample_poly(x, rate_to, rate)
+            assert_array_equal(y_to.shape, y_resamp.shape)
+            corr = np.corrcoef(y_to, y_resamp)[0, 1]
+            assert_(corr > 0.99, msg=corr)
+
 
 class TestCSpline1DEval(TestCase):