"Simplest Lowpass Filter" in Python
I'm currently working my way through Introduction to Digital Filters with Audio
Applications by Julius O. Smith III. The first section deals with what the book
calls the simplest lowpass filter. After the initial mathematical introduction
(both via tedious trigonometry and a complex approach) there's an actual
implementation. The book uses Matlab, but I much prefer Python for this type of
job even though I should probably use GNU Octave for this job but I really
prefer Python. These are the notes I took when converting that initial script.
The filter is described by:
$$ y(n) = x(n)+x(n-1), n = 1,2,...,N $$
This is the implementation in Matlab:
0% simplpm1.m - matlab main program implementing
1% the simplest lowpass filter:
2%
3% y(n) = x(n)+x(n-1)}
4
5N=10; % length of test input signal
6x = 1:N; % test input signal (integer ramp)
7B = [1,1]; % transfer function numerator
8A = 1; % transfer function denominator
9
10y = filter(B,A,x);
11
12for i=1:N
13 disp(sprintf('x(%d)=%f\ty(%d)=%f',i,x(i),i,y(i)));
14endThere's not much to it, we need to figure out what the scipy alternative is to
the Matlab filter function and make sure the indices are OK.
This is the translation I came up with:
0from scipy.signal import lfilter
1import numpy as np
2
3N=10
4x = np.arange(1.0,N+1)
5B = [1,1]
6A = 1
7
8y = lfilter(B,A,x);
9
10for i in range(N):
11 print(f"x({i+1})={x[i]} \t y({i+1})={y[i]}")As far as I can tell the Matlab function filter correspons the most to the
Python function lfilter. There's also the need to convert the code from
1-indexed to 0-indexed.
| X | Y |
|---|---|
| x(1)=1.0 | y(1)=1.0 |
| x(2)=2.0 | y(2)=3.0 |
| x(3)=3.0 | y(3)=5.0 |
| x(4)=4.0 | y(4)=7.0 |
| x(5)=5.0 | y(5)=9.0 |
| x(6)=6.0 | y(6)=11.0 |
| x(7)=7.0 | y(7)=13.0 |
| x(8)=8.0 | y(8)=15.0 |
| x(9)=9.0 | y(9)=17.0 |
| x(10)=10.0 | y(10)=19.0 |
This is the same result. scipy.signal.lfilter takes three parameters: b, a
and x. The documentation mentions that scipy.signal.sosfilt is actually the
preferred function. That's something to figure out at a later point though.
A second part of the exercise is a block oriented approach of the filter, where the filter state is saved and added as an additional parameter on the next call of the function.
0% simplpm2.m - block-oriented version of simplpm1.m
1
2N=10; % length of test input signal
3NB=N/2; % block length
4x = 1:N; % test input signal
5B = [1,1]; % feedforward coefficients
6A = 1; % feedback coefficients (no-feedback case)
7
8[y1, Sf] = filter(B,A,x(1:NB)); % process block 1
9 y2 = filter(B,A,x(NB+1:N),Sf); % process block 2
10
11for i=1:NB % print input and output for block 1
12 disp(sprintf('x(%d)=%f\ty(%d)=%f',i,x(i),i,y1(i)));
13end
14
15for i=NB+1:N % print input and output for block 2
16 disp(sprintf('x(%d)=%f\ty(%d)=%f',i,x(i),i,y2(i-NB)));
17endThe python implementation is a bit more finicky though:
The main difference is that you have to explicitly set the initial condition for
the filter for the scipy.signal.lfilter function to actually return the state
zf necessary for the next block. Setting it to [0.0] basically means that
the filter is at initial rest. It would probably be slightly more correct to use
scipy.signal.lfiltic for this, but it wouldn't make any difference. After that
it's just a matter of passing zf into the computation of the second block.
I found it easier to concatenate both blocks instead of having two loops.
Because these are numpy.ndarray you can't do the typical python trick of
using the + operator. Using numpy.concatenate does the trick though.
0import numpy as np
1
2N=10
3NB=int(N/2)
4x = np.arange(1.0,N+1)
5
6B = [1,1]
7A = 1
8
9y1, zf = lfilter(B,A,x[0:NB],zi=[0.0])
10y2, _ = lfilter(B,A,x[NB:N],zi=zf)
11
12y = np.concatenate((y1,y2))
13
14for i in range(N):
15 print(f"x({i+1})={x[i]} \t y({i+1})={y[i]}")The script gives the following output:
| X | Y |
|---|---|
| x(1)=1.0 | y(1)=1.0 |
| x(2)=2.0 | y(2)=3.0 |
| x(3)=3.0 | y(3)=5.0 |
| x(4)=4.0 | y(4)=7.0 |
| x(5)=5.0 | y(5)=9.0 |
| x(6)=6.0 | y(6)=11.0 |
| x(7)=7.0 | y(7)=13.0 |
| x(8)=8.0 | y(8)=15.0 |
| x(9)=9.0 | y(9)=17.0 |
| x(10)=10.0 | y(10)=19.0 |
This is exactly the same as above. Scipy and Numpy really are amazing tools
and in my experience really good at replacing something like Matlab, but there
are some pitfalls you have to watch out for when trying to do the conversion.