Repository: scipy/scipy

Issue:
scipy.signal.firls seems to be inefficient versus MATLAB firls
Hi everyone,
I've been porting some code over from MATLAB to Python, and I have run into an issue with scipy.signal.firls. It doesn't seem to be as fast/efficient as the MATLAB equivalent. I don't know if I'm using it wrong, but I wanted to report this and maybe get some guidance if I am indeed using it wrong.

Here is how I am executing the code in MATLAB. I've hard-coded the variables here for illustration purposes:
`firls(23220, [0, 0.000861326442721792, 0.000861326442721792, 1], [1 1 0 0]);`

This takes less than a couple of seconds to process.

Now in Python I do the following:
`scipy.signal.firls(23221, [0, 0.000861326442721792, 0.000861326442721792, 1], [1, 1, 0, 0])`

First thing you'll see is that argument 1 needs to have 1 added to it to make it an odd number. This I am guessing is related to the differences between Python and Matlab indexing functionality.

But when I run this, it takes over 5 minutes to process. I am guessing it's because of the large number of taps I am looking to process. 

Now if I sit and let it process it does give me the same output as MATLAB, roughly speaking. Which makes me think that for my purposes I'm not doing anything wrong. But can you spot anything here?

FWIW - I'm porting across a MATLAB library, which required me to also port across the MATLAB Resample class, and within that sits this firls line that is very slow in Python.

Any thoughts on how I can get past this?


Discussion:
The MATLAB documentation for their firls implementation states:

> b = firls(n,f,a) returns row vector b containing the n+1 coefficients of the order n FIR filter whose frequency-amplitude characteristics approximately match those given by vectors f and a. The output filter coefficients, or “taps,” in b obey the symmetry relation.

So is 'n' in their implementation equivalent to scipy.signal.firls(numtaps) parameter?

Resolving patch:
diff --git a/benchmarks/benchmarks/signal.py b/benchmarks/benchmarks/signal.py
--- a/benchmarks/benchmarks/signal.py
+++ b/benchmarks/benchmarks/signal.py
@@ -210,3 +210,14 @@ def setup(self, up, down, axis):
     def time_upfirdn2d(self, up, down, axis):
         for h, x in self.pairs:
             signal.upfirdn(h, x, up=up, down=down, axis=axis)
+
+
+class FIRLS(Benchmark):
+    param_names = ['n', 'edges']
+    params = [
+        [21, 101, 1001, 2001],
+        [(0.1, 0.9), (0.01, 0.99)],
+        ]
+
+    def time_firls(self, n, edges):
+        signal.firls(n, (0,) + edges + (1,), [1, 1, 0, 0])
diff --git a/scipy/signal/fir_filter_design.py b/scipy/signal/fir_filter_design.py
--- a/scipy/signal/fir_filter_design.py
+++ b/scipy/signal/fir_filter_design.py
@@ -9,7 +9,8 @@
 import numpy as np
 from numpy.fft import irfft, fft, ifft
 from scipy.special import sinc
-from scipy.linalg import toeplitz, hankel, pinv
+from scipy.linalg import (toeplitz, hankel, solve, LinAlgError, LinAlgWarning,
+                          lstsq)
 from scipy._lib.six import string_types
 
 from . import sigtools
@@ -965,6 +966,8 @@ def firls(numtaps, bands, desired, weight=None, nyq=None, fs=None):
     bands = np.asarray(bands).flatten() / nyq
     if len(bands) % 2 != 0:
         raise ValueError("bands must contain frequency pairs.")
+    if (bands < 0).any() or (bands > 1).any():
+        raise ValueError("bands must be between 0 and 1 relative to Nyquist")
     bands.shape = (-1, 2)
 
     # check remaining params
@@ -1032,8 +1035,20 @@ def firls(numtaps, bands, desired, weight=None, nyq=None, fs=None):
     b[1:] += m * np.cos(n[1:] * np.pi * bands) / (np.pi * n[1:]) ** 2
     b = np.dot(np.diff(b, axis=2)[:, :, 0], weight)
 
-    # Now we can solve the equation (use pinv because Q can be rank deficient)
-    a = np.dot(pinv(Q), b)
+    # Now we can solve the equation
+    try:  # try the fast way
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter('always')
+            a = solve(Q, b, sym_pos=True, check_finite=False)
+        for ww in w:
+            if (ww.category == LinAlgWarning and
+                    str(ww.message).startswith('Ill-conditioned matrix')):
+                raise LinAlgError(str(ww.message))
+    except LinAlgError:  # in case Q is rank deficient
+        # This is faster than pinvh, even though we don't explicitly use
+        # the symmetry here. gelsy was faster than gelsd and gelss in
+        # some non-exhaustive tests.
+        a = lstsq(Q, b, lapack_driver='gelsy')[0]
 
     # make coefficients symmetric (linear phase)
     coeffs = np.hstack((a[:0:-1], 2 * a[0], a[1:]))


Test patch:
diff --git a/scipy/signal/tests/test_fir_filter_design.py b/scipy/signal/tests/test_fir_filter_design.py
--- a/scipy/signal/tests/test_fir_filter_design.py
+++ b/scipy/signal/tests/test_fir_filter_design.py
@@ -5,9 +5,10 @@
                            assert_equal, assert_,
                            assert_allclose, assert_warns)
 from pytest import raises as assert_raises
+import pytest
 
+from scipy.linalg import LinAlgWarning
 from scipy.special import sinc
-
 from scipy.signal import kaiser_beta, kaiser_atten, kaiserord, \
         firwin, firwin2, freqz, remez, firls, minimum_phase
 
@@ -536,6 +537,27 @@ def test_compare(self):
         taps = firls(7, (0, 1, 2, 3, 4, 5), [1, 0, 0, 1, 1, 0], nyq=10)
         assert_allclose(taps, known_taps)
 
+        with pytest.raises(ValueError, match='between 0 and 1'):
+            firls(7, [0, 1], [0, 1], nyq=0.5)
+
+    def test_rank_deficient(self):
+        # solve() runs but warns (only sometimes, so here we don't use match)
+        x = firls(21, [0, 0.1, 0.9, 1], [1, 1, 0, 0])
+        w, h = freqz(x, fs=2.)
+        assert_allclose(np.abs(h[:2]), 1., atol=1e-5)
+        assert_allclose(np.abs(h[-2:]), 0., atol=1e-6)
+        # switch to pinvh (tolerances could be higher with longer
+        # filters, but using shorter ones is faster computationally and
+        # the idea is the same)
+        x = firls(101, [0, 0.01, 0.99, 1], [1, 1, 0, 0])
+        w, h = freqz(x, fs=2.)
+        mask = w < 0.01
+        assert mask.sum() > 3
+        assert_allclose(np.abs(h[mask]), 1., atol=1e-4)
+        mask = w > 0.99
+        assert mask.sum() > 3
+        assert_allclose(np.abs(h[mask]), 0., atol=1e-4)
+
 
 class TestMinimumPhase(object):