Butterworth Filter Design: Theory and Python Implementation

From the maximally flat magnitude derivation of the Butterworth filter to analog-to-digital conversion and practical design with SciPy.

Introduction

The Butterworth filter is an IIR filter with a maximally flat magnitude response in the passband. Published by Stephen Butterworth in 1930, it features a smooth frequency response with no ripple and is one of the most widely used filter designs in signal processing.

This article covers the mathematical foundations of the Butterworth filter through practical design and implementation with SciPy.

Magnitude Response

Magnitude Squared Function

The magnitude squared function of an \(N\)-th order Butterworth lowpass filter is:

\[|H(j\Omega)|^2 = \frac{1}{1 + \left(\frac{\Omega}{\Omega_c}\right)^{2N}} \tag{1}\]

where \(\Omega_c\) is the cutoff angular frequency (-3dB point) and \(N\) is the filter order.

Maximally Flat Property

Taylor expansion of equation \((1)\) around \(\Omega = 0\) shows that the first \(2N-1\) derivatives of \(|H(j\Omega)|^2\) are all zero. This is why it is called “maximally flat.”

Behavior by Frequency

  • \(\Omega = 0\): \(|H| = 1\) (perfect passthrough)
  • \(\Omega = \Omega_c\): \(|H| = 1/\sqrt{2} \approx -3\,\text{dB}\)
  • \(\Omega \gg \Omega_c\): \(|H| \approx (\Omega_c/\Omega)^N\) (roll-off rate: \(-20N\) dB/decade)

Pole Locations

Butterworth filter poles are equally spaced on a circle of radius \(\Omega_c\) (the Butterworth circle) in the s-plane. The \(k\)-th pole of an \(N\)-th order filter is:

\[s_k = \Omega_c \exp\left(j\frac{\pi(2k + N - 1)}{2N}\right), \quad k = 1, 2, \ldots, N \tag{2}\]

Only poles in the left half-plane (negative real part) are used for a stable filter.

Design Procedure

1. Determine Filter Order

Compute the required order from passband/stopband specifications:

\[N \geq \frac{\log(\varepsilon_s^2/\varepsilon_p^2)}{2\log(\Omega_s/\Omega_p)} \tag{3}\]

2. Analog Prototype Design

Design a normalized Butterworth filter with \(\Omega_c = 1\).

3. Digital Conversion

Convert the analog filter to digital using the bilinear transform:

\[s = \frac{2}{T}\frac{1 - z^{-1}}{1 + z^{-1}} \tag{4}\]

Pre-warping compensates for the frequency axis distortion:

\[\Omega_a = \frac{2}{T}\tan\left(\frac{\omega_d T}{2}\right) \tag{5}\]

Python Implementation

Filter Design and Frequency Response with SciPy

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

# --- Filter design ---
fs = 1000           # Sampling frequency [Hz]
fc = 100            # Cutoff frequency [Hz]
orders = [2, 4, 8]  # Filter orders

fig, axes = plt.subplots(2, 1, figsize=(10, 8))

for N in orders:
    sos = signal.butter(N, fc, fs=fs, output='sos')
    w, h = signal.sosfreqz(sos, worN=4096, fs=fs)

    axes[0].plot(w, 20 * np.log10(np.abs(h) + 1e-12), label=f'N={N}')
    axes[1].plot(w, np.degrees(np.unwrap(np.angle(h))), label=f'N={N}')

axes[0].set_xlabel('Frequency [Hz]')
axes[0].set_ylabel('Magnitude [dB]')
axes[0].set_title('Butterworth Filter - Magnitude Response')
axes[0].set_xlim(0, 500)
axes[0].set_ylim(-80, 5)
axes[0].axvline(fc, color='gray', linestyle='--', alpha=0.5)
axes[0].axhline(-3, color='gray', linestyle=':', alpha=0.5, label='-3 dB')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_xlabel('Frequency [Hz]')
axes[1].set_ylabel('Phase [degrees]')
axes[1].set_title('Butterworth Filter - Phase Response')
axes[1].set_xlim(0, 500)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Noise Removal Application

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

# --- Generate signal ---
fs = 1000
t = np.arange(0, 1, 1/fs)
clean = np.sin(2 * np.pi * 10 * t) + 0.5 * np.sin(2 * np.pi * 30 * t)
noisy = clean + 0.8 * np.random.randn(len(t))

# --- Design and apply Butterworth filter ---
sos = signal.butter(4, 50, fs=fs, output='sos')
filtered_causal = signal.sosfilt(sos, noisy)       # Causal filter
filtered_zero = signal.sosfiltfilt(sos, noisy)      # Zero-phase filter

# --- Plot ---
fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)

axes[0].plot(t, noisy, alpha=0.5, label='Noisy')
axes[0].plot(t, clean, 'k', linewidth=1.5, label='Original')
axes[0].set_title('Input Signal')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(t, filtered_causal, label='sosfilt (causal)')
axes[1].plot(t, clean, 'k', linewidth=1.5, label='Original')
axes[1].set_title('Causal Filtering (sosfilt)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

axes[2].plot(t, filtered_zero, label='sosfiltfilt (zero-phase)')
axes[2].plot(t, clean, 'k', linewidth=1.5, label='Original')
axes[2].set_title('Zero-Phase Filtering (sosfiltfilt)')
axes[2].set_xlabel('Time [s]')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

sosfilt is a causal filter (with phase delay), while sosfiltfilt achieves zero-phase by filtering forward and backward, but cannot be used for real-time processing.

Comparison with Other IIR Filters

FilterPassbandStopbandTransition BandPhase
ButterworthMaximally flatMonotonicWideRelatively smooth
Chebyshev Type IEquirippleMonotonicNarrowSteep changes
Chebyshev Type IIMonotonicEquirippleNarrowRelatively smooth
Elliptic (Cauer)EquirippleEquirippleNarrowestSteepest

For the same order, elliptic filters have the sharpest transition band but introduce passband ripple. Butterworth is optimal when ripple is unacceptable.

References

  • Butterworth, S. (1930). “On the theory of filter amplifiers”. Wireless Engineer, 7(6), 536-541.
  • Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
  • SciPy scipy.signal.butter documentation