Digital filters are essential tools in signal processing, helping us remove unwanted noise or extract useful parts of a signal. Among digital filters, IIR (Infinite Impulse Response) filters are very common because they can achieve sharp frequency responses with relatively low computational cost.
In this post, we will walk through the basic design of a simple IIR low-pass filter, starting from an analog filter, converting it to the digital domain, and finally implementing it in Python and MATLAB/Octave. Don’t worry if you are new to this, each step is explained clearly.
Step 1: Start from the Analog Prototype
We begin with the analog transfer function of a first-order low-pass filter:
$$G(s) = \frac{\omega_0}{s+\omega_0}$$
Here:
- \(s\) is the Laplace variable,
- \(\omega_0\) is the cutoff frequency in radians per second.
The angular cutoff frequency is defined as:
$$\omega_0 = 2\pi f_0$$
where \(f_0\) is the cutoff frequency in Hz.
Step 2: Bilinear Transformation
To move from the analog domain (\(s\)) to the digital domain (\(z\)), we use the bilinear transform, which maps the analog \(s\)-plane to the digital \(z\)-plane. This requires defining a frequency pre-warping term:
$$K=\frac{\omega_0}{\tan{(\frac{\omega_0T_s}{2})}}$$
where \(T_s\) is the sampling period (inverse of the sampling frequency \(f_s\)).
Applying the bilinear transform gives us the digital transfer function:
$$G(z) = \frac{\omega_0}{K\left(\frac{1-z^{-1}}{1+z^{-1}}\right) + \omega_0}$$
After algebraic manipulation, we can rewrite it as:
$$G(z) = \frac{\omega_0(1+z^{-1})}{K(1-z^{-1})+\omega_0(1+z^{-1})}$$
Expanding further:
$$G(z) = \frac{\omega_0+\omega_0z^{-1}}{K – K z^{-1}+\omega_0+\omega_0z^{-1}}$$
Finally, in standard digital filter form:
$$G(z) = \frac{\omega_0z^{-1}+\omega_0}{z^{-1}(\omega_0 – K) + K + \omega_0}$$
Step 3: Coefficients for Implementation
From the transfer function, the filter coefficients can be written as:
- Denominator (feedback) coefficients:
\(a_1 = \omega_0-K,\quad a_0 = K+\omega_0\) - Numerator (feedforward) coefficients:
\(b_1 = \omega_0,\quad b_0 = \omega_0\)
Step 4: Implementation in Python (Manual + Built-in)
Manual Coefficient Calculation
import numpy as np
from scipy.signal import lfilter
# Parameters
fs = 1000 # Sampling frequency (Hz)
f0 = 50 # Cutoff frequency (Hz)
Ts = 1/fs
omega0 = 2*np.pi*f0
K = omega0 / np.tan(omega0*Ts/2)
# Coefficients
b = [omega0, omega0]
a = [K + omega0, omega0 - K]
# Example input signal (sine + noise)
t = np.linspace(0, 1, fs, endpoint=False)
x = np.sin(2*np.pi*10*t) + 0.5*np.sin(2*np.pi*200*t)
# Apply filter
y = lfilter(b, a, x)
Using Built-in SciPy Function
Instead of calculating coefficients manually, SciPy provides functions like butter (Butterworth filter):
import numpy as np
from scipy.signal import butter, filtfilt
# Parameters
fs = 1000 # Sampling frequency (Hz)
f0 = 50 # Cutoff frequency (Hz)
# Example input signal (sine + noise)
t = np.linspace(0, 1, fs, endpoint=False)
x = np.sin(2*np.pi*10*t) + 0.5*np.sin(2*np.pi*200*t)
# Normalized cutoff frequency (relative to Nyquist)
Wn = f0 / (fs/2)
b, a = butter(1, Wn, btype='low')
# Apply zero-phase filtering
y_built = filtfilt(b, a, x)
Step 5: Implementation in MATLAB / Octave (Manual + Built-in)
Manual Coefficient Calculation
fs = 1000; % Sampling frequency
f0 = 50; % Cutoff frequency
Ts = 1/fs;
omega0 = 2*pi*f0;
K = omega0 / tan(omega0*Ts/2);
b = [omega0 omega0];
a = [K+omega0 omega0-K];
t = 0:1/fs:1-1/fs;
x = sin(2*pi*10*t) + 0.5*sin(2*pi*200*t);
y = filter(b, a, x);
Using Built-in MATLAB/Octave Function
MATLAB/Octave also provides butter for easy filter design:
fs = 1000; % Sampling frequency
f0 = 50; % Cutoff frequency
t = 0:1/fs:1-1/fs;
x = sin(2*pi*10*t) + 0.5*sin(2*pi*200*t);
Wn = f0/(fs/2); % Normalized cutoff frequency
[b,a] = butter(1, Wn, 'low');
y_built = filtfilt(b, a, x);
Why We Use butter
and How It Differs from Other Filters
In the examples above, we used the function butter
to design the filter. This function creates a Butterworth filter, which is one of the most common IIR filter types. The key reason is its maximally flat frequency response in the passband — meaning it does not introduce ripples and ensures a smooth, monotonic transition from passband to stopband. This makes it a safe default choice for many applications.
However, Butterworth is not the only type of IIR filter. Different design methods give different trade-offs between passband smoothness, stopband attenuation, and transition sharpness.
Function Calls in Python and MATLAB/Octave
The table below shows how you would call different filter design functions. Notice that the syntax is very similar across filter families, which makes experimenting easy.
Filter Type | Python (SciPy) | MATLAB / Octave |
---|---|---|
Butterworth | b,a = butter(N, Wn, 'low') | [b,a] = butter(N, Wn, 'low') |
Chebyshev Type I | b,a = cheby1(N, rp, Wn, 'low') | [b,a] = cheby1(N, rp, Wn, 'low') |
Chebyshev Type II | b,a = cheby2(N, rs, Wn, 'low') | [b,a] = cheby2(N, rs, Wn, 'low') |
Elliptic (Cauer) | b,a = ellip(N, rp, rs, Wn, 'low') | [b,a] = ellip(N, rp, rs, Wn, 'low') |
Bessel | b,a = bessel(N, Wn, 'low', norm='phase') | [b,a] = besself(N, Wn) (analog, needs bilinear transform for digital) |
Here:
- \(N\) = filter order (e.g., 1, 2, 3, 4, …)
- \(Wn\) = normalized cutoff frequency (relative to Nyquist frequency).
- \(rp\) = passband ripple (dB), used in Chebyshev I and Elliptic.
- \(rs\) = stopband attenuation (dB), used in Chebyshev II and Elliptic.
As you can see, once you understand the syntax for one filter, switching to another is straightforward.
Descriptions and Use Cases
Now let’s look at how these filters behave and when you might choose each one.
Filter Type | Main Characteristics | When to Use |
---|---|---|
Butterworth | – Maximally flat passband (no ripples) – Smooth frequency response – Slower roll-off compared to others | General-purpose filtering when stability and smooth response are most important. |
Chebyshev Type I | – Ripples allowed in the passband – Steeper roll-off than Butterworth | When you need a sharper transition and can accept passband ripples. |
Chebyshev Type II | – Flat passband (no ripples) – Ripples in the stopband – Steeper roll-off than Butterworth | When a smooth passband is required but sharper attenuation is still needed. |
Elliptic (Cauer) | – Ripples in both passband and stopband – Sharpest roll-off for a given order | When very tight specifications are needed, e.g., in communications systems. |
Bessel | – Maximally linear phase (preserves waveform shape) – Poor frequency selectivity | When phase linearity is critical, e.g., audio or biomedical signals. |
Which One Should You Choose?
- Butterworth is the go-to choice for beginners and general use.
- Chebyshev (I & II) provide sharper transitions at the cost of ripples.
- Elliptic filters give the sharpest roll-off but with ripples on both sides.
- Bessel filters are less about frequency and more about preserving the shape of signals.
This is why libraries include all of them: the “best” filter depends on what you value most — flatness, sharpness, attenuation, or phase linearity.
Conclusion
We have designed a simple first-order IIR low-pass filter starting from the analog transfer function, applied the bilinear transform, derived the coefficients, and finally implemented it manually in Python and MATLAB/Octave.
To make life easier, we also showed how to use built-in filter design functions (butter in SciPy and MATLAB/Octave), which are the preferred approach in practice.
With these tools, you can easily create filters for different cutoff frequencies and orders, opening the door to more advanced designs like Butterworth, Chebyshev, and elliptic filters.
0 Comments