I'm designing a low-pass filter for a digital signal processing application that ideally just passes a very small bandwidth above DC. I'm using an IIR biquad filter for this, where the coefficients are derived using the instructions here. A smaller bandwidth leads to a longer filtering time (larger time constant) but yields a more accurate result whereas a larger bandwidth can be filtered faster but is less accurate. Both of these are valid use cases.
Here's the code I've got
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import freqz
# calculates filter coefficients using link above
# fc is corner frequency, fs is sample freq
def iir_lp_coeffs(fc, fs):
w0 = 2 * np.pi * fc / fs
q = 1 / np.sqrt(2)
alpha = np.sin(w0) / (2 * q)
b0 = (1 - np.cos(w0)) / 2
b1 = 1 - np.cos(w0)
b2 = b0
a0 = 1 + alpha
a1 = -2 * np.cos(w0)
a2 = 1 - alpha
b0 /= a0
b1 /= a0
b2 /= a0
a1 /= a0
a2 /= a0
a0 /= a0
return (
np.array([b0, b1, b2], dtype=np.float64),
np.array([a0, a1, a2], dtype=np.float64),
)
fc = 2 # low pass corner frequency (Hz)
fsample = 500e3
b, a = iir_lp_coeffs(fc, fsample)
w, h = freqz(b, a, worN=int(1e6), fs=fsample)
fig, ax = plt.subplots()
ax.plot(w, 20 * np.log10(abs(h)))
ax.set_ylim(-40, 10)
ax.set_xscale("log")
plt.show()
print(w[0:10])
print(abs(h[0:10]))
The current settings use 64-bit floating point with a cutoff frequency of $2\,\text{Hz}$. This all works fine, and I can even decrease the corner frequency substantially as long as I increase the granularity of freqz
(with worN=
).
For instance here's a plot of the gain response with the above code (note that I've cut the x axis off at the higher frequencies):
However, my actual application requires 32-bit floating point. When I do this (set dtype
of iir_lp_coeffs
to np.float32
), I get non-unity gain in the passband. For instance, here's a gain response with fc=10
using 32-bit:
If I set the corner frequency higher, the gain response looks correct again (e.g., fc=100
looks fine).
Am I running up against the limit of what's possible with 32-bit FP? Or, is there another strategy that would allow me to get away with the lower precision of 32-bit? Have I correctly diagnosed this issue as a floating-point issue?