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.

Rate this post

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *