Repository: scipy/scipy

Issue:
Add standard filter types to firwin to set pass_zero intuitively (feature request)
<<Please describe the issue in detail here, and for bug reports fill in the fields below.>>

In order to create standard filter responses with firwin one needs to undestand the pass_zero parameter. The parameter is internally very useful to set the frequency response at DC but from the outside it's pretty diabolical as it even switches between True/False depending on the number of arguments. I had to have a look at the code of firwin to make sense of it.

For example:
h = firtest.firwin(999,f2*2,pass_zero=False) gives me a highpass whereas
h = firtest.firwin(999,f2*2) gives me a lowpass because pass_zero is True here by default.
which becomes only clear after understanding the parameter pass_zero.
It's even more confusing when looking at:
h = firtest.firwin(999,[f1*2,f2*2])
which gives a bandstop because pass_zero is now False (!) by default.
I'm sure there is a reason behind this but it's not very userfriendly.

Feature request:
Add a parameter called "filtertype" which sets pass_zero and the number of parameters expected as a sanity check:

```
    type2args = { 'lp':[1,True], 'lowpass':[1,True],
                  'hp':[1,False], 'highpass':[1,False],
                  'bs':[2,True], 'bandstop':[2,True],
                  'bp':[2,False], 'bandpass':[2,False] }

    if filtertype is not None:
        ncutoff,pass_zero = type2args[filtertype]
        if ncutoff != cutoff.size:
            raise ValueError("Number of cutoff freq expected:",ncutoff)


```

Essentially just an associative array which keeps the keywords and the expected parameters.

![figure_1](https://user-images.githubusercontent.com/1679394/31583971-ce40b932-b19c-11e7-94a2-0ca66b4e6774.png)
[firwin_fr2.zip](https://github.com/scipy/scipy/files/1385359/firwin_fr2.zip)


I've attached a file containing the original firwin command, a new version with that array in it and the diff. 

I'd be happy to do a pull request if people here like that idea!

Best,
/Bernd Porr

PS: that's a clip I made about this command as part of my teaching which would be much neater with standard parameters for high/low/bs/bp: https://www.youtube.com/watch?v=GpiL9vqQqlE


Discussion:
I agree the `pass_dc` is probably not as intuitive as specifying the filter type, but this function is 7 years old so removing that argument would be difficult from a backward compatibility / deprecation perspective. We could consider adding a `ftype`-like argument and ensuring only one of these two is specified, along the lines of what you suggest, which could be useful. I'm +0 on this because I don't find the `pass_dc` too difficult to work with.

@WarrenWeckesser looks like you worked on this at least 7 years ago. Thoughts on changing the API?
Thanks for the quick comment. As you say it needs to be 100% backwards compatible. The pass_dc parameter is great for geeks like us or people who want to do multiple notches etc. 
I'm thinking of the casual user who just quickly needs a high/low/etc pass.
Indeed. That's what I thought that these arguments then are mutually exclusive and won't break any existing python code using this command.

Resolving patch:
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
@@ -3,6 +3,7 @@
 from __future__ import division, print_function, absolute_import
 
 from math import ceil, log
+import operator
 import warnings
 
 import numpy as np
@@ -292,9 +293,13 @@ def firwin(numtaps, cutoff, width=None, window='hamming', pass_zero=True,
     window : string or tuple of string and parameter values, optional
         Desired window to use. See `scipy.signal.get_window` for a list
         of windows and required parameters.
-    pass_zero : bool, optional
+    pass_zero : {True, False, 'bandpass', 'lowpass', 'highpass', 'bandstop'}, optional
         If True, the gain at the frequency 0 (i.e. the "DC gain") is 1.
-        Otherwise the DC gain is 0.
+        If False, the DC gain is 0. Can also be a string argument for the
+        desired filter type (equivalent to ``btype`` in IIR design functions).
+
+        .. versionadded:: 1.3.0
+           Support for string arguments.
     scale : bool, optional
         Set to True to scale the coefficients so that the frequency
         response is exactly unity at a certain frequency.
@@ -376,7 +381,7 @@ def firwin(numtaps, cutoff, width=None, window='hamming', pass_zero=True,
     >>> signal.firwin(numtaps, [f1, f2, f3, f4], pass_zero=False)
     array([ 0.04890915,  0.91284326,  0.04890915])
 
-    """
+    """  # noqa: E501
     # The major enhancements to this function added in November 2010 were
     # developed by Tom Krauss (see ticket #902).
 
@@ -404,6 +409,35 @@ def firwin(numtaps, cutoff, width=None, window='hamming', pass_zero=True,
         beta = kaiser_beta(atten)
         window = ('kaiser', beta)
 
+    if isinstance(pass_zero, str):
+        if pass_zero in ('bandstop', 'lowpass'):
+            if pass_zero == 'lowpass':
+                if cutoff.size != 1:
+                    raise ValueError('cutoff must have one element if '
+                                     'pass_zero=="lowpass", got %s'
+                                     % (cutoff.shape,))
+            elif cutoff.size <= 1:
+                raise ValueError('cutoff must have at least two elements if '
+                                 'pass_zero=="bandstop", got %s'
+                                 % (cutoff.shape,))
+            pass_zero = True
+        elif pass_zero in ('bandpass', 'highpass'):
+            if pass_zero == 'highpass':
+                if cutoff.size != 1:
+                    raise ValueError('cutoff must have one element if '
+                                     'pass_zero=="highpass", got %s'
+                                     % (cutoff.shape,))
+            elif cutoff.size <= 1:
+                raise ValueError('cutoff must have at least two elements if '
+                                 'pass_zero=="bandpass", got %s'
+                                 % (cutoff.shape,))
+            pass_zero = False
+        else:
+            raise ValueError('pass_zero must be True, False, "bandpass", '
+                             '"lowpass", "highpass", or "bandstop", got '
+                             '%s' % (pass_zero,))
+    pass_zero = bool(operator.index(pass_zero))  # ensure bool-like
+
     pass_nyquist = bool(cutoff.size & 1) ^ pass_zero
     if pass_nyquist and numtaps % 2 == 0:
         raise ValueError("A filter with an even number of coefficients must "


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
@@ -131,7 +131,8 @@ class TestFirWinMore(object):
     def test_lowpass(self):
         width = 0.04
         ntaps, beta = kaiserord(120, width)
-        taps = firwin(ntaps, cutoff=0.5, window=('kaiser', beta), scale=False)
+        kwargs = dict(cutoff=0.5, window=('kaiser', beta), scale=False)
+        taps = firwin(ntaps, **kwargs)
 
         # Check the symmetry of taps.
         assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
@@ -142,6 +143,9 @@ def test_lowpass(self):
         assert_array_almost_equal(np.abs(response),
                                     [1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
 
+        taps_str = firwin(ntaps, pass_zero='lowpass', **kwargs)
+        assert_allclose(taps, taps_str)
+
     def test_highpass(self):
         width = 0.04
         ntaps, beta = kaiserord(120, width)
@@ -149,8 +153,8 @@ def test_highpass(self):
         # Ensure that ntaps is odd.
         ntaps |= 1
 
-        taps = firwin(ntaps, cutoff=0.5, window=('kaiser', beta),
-                        pass_zero=False, scale=False)
+        kwargs = dict(cutoff=0.5, window=('kaiser', beta), scale=False)
+        taps = firwin(ntaps, pass_zero=False, **kwargs)
 
         # Check the symmetry of taps.
         assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
@@ -161,11 +165,14 @@ def test_highpass(self):
         assert_array_almost_equal(np.abs(response),
                                     [0.0, 0.0, 0.0, 1.0, 1.0, 1.0], decimal=5)
 
+        taps_str = firwin(ntaps, pass_zero='highpass', **kwargs)
+        assert_allclose(taps, taps_str)
+
     def test_bandpass(self):
         width = 0.04
         ntaps, beta = kaiserord(120, width)
-        taps = firwin(ntaps, cutoff=[0.3, 0.7], window=('kaiser', beta),
-                        pass_zero=False, scale=False)
+        kwargs = dict(cutoff=[0.3, 0.7], window=('kaiser', beta), scale=False)
+        taps = firwin(ntaps, pass_zero=False, **kwargs)
 
         # Check the symmetry of taps.
         assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
@@ -177,11 +184,15 @@ def test_bandpass(self):
         assert_array_almost_equal(np.abs(response),
                 [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0], decimal=5)
 
-    def test_multi(self):
+        taps_str = firwin(ntaps, pass_zero='bandpass', **kwargs)
+        assert_allclose(taps, taps_str)
+
+    def test_bandstop_multi(self):
         width = 0.04
         ntaps, beta = kaiserord(120, width)
-        taps = firwin(ntaps, cutoff=[0.2, 0.5, 0.8], window=('kaiser', beta),
-                        pass_zero=True, scale=False)
+        kwargs = dict(cutoff=[0.2, 0.5, 0.8], window=('kaiser', beta),
+                      scale=False)
+        taps = firwin(ntaps, **kwargs)
 
         # Check the symmetry of taps.
         assert_array_almost_equal(taps[:ntaps//2], taps[ntaps:ntaps-ntaps//2-1:-1])
@@ -195,6 +206,9 @@ def test_multi(self):
                 [1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
                 decimal=5)
 
+        taps_str = firwin(ntaps, pass_zero='bandstop', **kwargs)
+        assert_allclose(taps, taps_str)
+
     def test_fs_nyq(self):
         """Test the fs and nyq keywords."""
         nyquist = 1000
@@ -245,6 +259,19 @@ def test_even_highpass_raises_value_error(self):
         assert_raises(ValueError, firwin, 40, 0.5, pass_zero=False)
         assert_raises(ValueError, firwin, 40, [.25, 0.5])
 
+    def test_bad_pass_zero(self):
+        """Test degenerate pass_zero cases."""
+        with assert_raises(ValueError, match='pass_zero must be'):
+            firwin(41, 0.5, pass_zero='foo')
+        with assert_raises(TypeError, match='cannot be interpreted'):
+            firwin(41, 0.5, pass_zero=1.)
+        for pass_zero in ('lowpass', 'highpass'):
+            with assert_raises(ValueError, match='cutoff must have one'):
+                firwin(41, [0.5, 0.6], pass_zero=pass_zero)
+        for pass_zero in ('bandpass', 'bandstop'):
+            with assert_raises(ValueError, match='must have at least two'):
+                firwin(41, [0.5], pass_zero=pass_zero)
+
 
 class TestFirwin2(object):