IIR filters

import numpy as np
import matplotlib.pyplot as plt

import sdr

%config InlineBackend.print_figure_kwargs = {"facecolor" : "w"}
# %matplotlib widget

Create an IIR filter

The user creates an IIR filter with the sdr.IIR class by specifying the feedforward coefficients \(b_i\) and feedback coefficients \(a_j\). Alternatively, an IIR filter may be created by specifying the zeros and poles in sdr.IIR.ZerosPoles.

Below is an IIR filter with one real zero and two complex-conjugate poles.

zero = 0.6
pole = 0.8 * np.exp(1j * np.pi / 8)
iir = sdr.IIR.ZerosPoles([zero], [pole, pole.conj()])
print(iir)
<sdr.IIR object at 0x000002214B040A88>
print(f"Feedforward taps: {iir.b_taps}")
print(f"Feedback taps: {iir.a_taps}")
Feedforward taps: [ 1.  -0.6]
Feedback taps: [ 1.         -1.47820725  0.64      ]

Examine the impulse response, \(h[n]\)

The impulse response of the IIR filter is computed and returned from the sdr.IIR.impulse_response() method. The impulse response \(h[n]\) is the output of the filter when the input is an impulse \(\delta[n]\).

h = iir.impulse_response(30)
print(h)
[ 1.00000000e+00  8.78207252e-01  6.58172329e-01  4.10862468e-01
  1.86109590e-01  1.21565653e-02 -1.01140214e-01 -1.57286400e-01
 -1.67772160e-01 -1.47338728e-01 -1.10422993e-01 -6.89312837e-02
 -3.12240078e-02 -2.03953322e-03  1.69685122e-02  2.63882791e-02
  2.81474977e-02  2.47193366e-02  1.85259041e-02  1.15647504e-02
  5.23851924e-03  3.42176895e-04 -2.84684395e-03 -4.42721858e-03
 -4.72236648e-03 -4.14721649e-03 -3.10813095e-03 -1.94024315e-03
 -8.78877688e-04 -5.74077567e-05]

The impulse response is conveniently plotted using the sdr.plot.impulse_response() function.

plt.figure(figsize=(10, 5))
sdr.plot.impulse_response(iir.b_taps, iir.a_taps, 30, marker=".")
plt.show()
../../_images/0051bf116786166316cb1ce42703c32e9881d8d27ce2866400cf5765cd400b92.png

Examine the step response, \(s[n]\)

The step response of the IIR filter is computed and returned from the sdr.IIR.step_response() method. The step response \(s[n]\) is the output of the filter when the input is a unit step \(u[n]\).

s = iir.step_response(30)
print(s)
[1.         1.87820725 2.53637958 2.94724205 3.13335164 3.1455082
 3.04436799 2.88708159 2.71930943 2.5719707  2.46154771 2.39261642
 2.36139242 2.35935288 2.3763214  2.40270968 2.43085717 2.45557651
 2.47410241 2.48566716 2.49090568 2.49124786 2.48840102 2.4839738
 2.47925143 2.47510421 2.47199608 2.47005584 2.46917696 2.46911955]

The step response is conveniently plotted using the sdr.plot.step_response() function.

plt.figure(figsize=(10, 5))
sdr.plot.step_response(iir.b_taps, iir.a_taps, 30, marker=".")
plt.show()
../../_images/e727b5aa6ab39a1244a0887a37cca5e77a2addc5b0567dab8f78397c331b3f78.png

Examine the zeros and poles

Zeros are \(z\) values that set the numerator of \(H(z)\) to zero.

print(iir.zeros)
[0.6]

Poles are \(z\) values that set the denominator of \(H(z)\) to zero. The poles define the stability of the IIR filter.

print(iir.poles)
[0.73910363+0.30614675j 0.73910363-0.30614675j]

The zeros and poles are conveniently plotted in the complex plane using the sdr.plot.zeros_poles() function.

plt.figure(figsize=(5, 5))
sdr.plot.zeros_poles(iir.b_taps, iir.a_taps)
plt.show()
../../_images/66cfb2cc980d704e292a3dcd9e84a3cb500020f14bab82adabac385e8bfdee25.png

Examine the frequency response, \(H(\omega)\)

The frequency response is the transfer function \(H(z)\) evaluated at the complex exponential \(e^{j \omega}\), where \(\omega = 2 \pi f / f_s\).

The two-sided frequency response is conveniently plotted using the sdr.plot.frequency_response() function.

plt.figure(figsize=(10, 5))
sdr.plot.frequency_response(iir.b_taps, iir.a_taps)
plt.show()
../../_images/809518d83290aef073d2a52ed71509bf7dda87612bb555094f0dbf7ad890a83a.png

The one-sided frequency response, with logarithmic scale, can be plotted using the x_axis="log" keyword argument.

plt.figure(figsize=(10, 5))
sdr.plot.frequency_response(iir.b_taps, iir.a_taps, x_axis="log")
plt.show()
../../_images/e0650cba12b978476aceb5bbb376021b8445b1a7e3e15b994faa5dd9726f08c4.png

Examine the group delay, \(\tau_g(\omega)\)

The group delay \(\tau_g(\omega)\) is the time shift of the envelope of a signal passed through the filter as a function of its frequency \(\omega\).

The group delay is conveniently plotted using the sdr.plot.group_delay() function.

plt.figure(figsize=(10, 5))
sdr.plot.group_delay(iir.b_taps, iir.a_taps)
plt.show()
../../_images/8e25ab9e1fa8c1b04d8416348fcc5a7ea70e6c45c637eaabe279aa1498093f5f.png
plt.figure(figsize=(10, 5))
sdr.plot.group_delay(iir.b_taps, iir.a_taps, x_axis="log")
plt.show()
../../_images/54a449bde05c50d562f516f9e5c2cb99381658e5108ce04c75dffaca176d00cf.png

Fully analyze an IIR filter

The user can easily analyze the perform of a given IIR filter using the sdr.plot.filter() function.

Here is an IIR filter with one real zero and 8 complex poles.

zeros = np.array([0.8])
poles = 0.6 * np.exp(1j * np.linspace(np.pi / 8, np.pi / 4, 4, endpoint=False))
poles = np.concatenate((poles, poles.conj()))
iir = sdr.IIR.ZerosPoles(zeros, poles)
print(iir)
<sdr.IIR object at 0x000002214DC574C8>
plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/5489800422dd62d934a0cab14c586d679e4d6832ee1d5b136928a917473e051b.png

Poles and digital filter stability

Reference:

  • R. Lyons, Understanding Digital Signal Processing 3rd Edition, Section 6.3.1.

When the pole is real and inside the unit circle, the impulse response \(h[n]\) is an exponential decay.

zeros = []
poles = [0.8]
iir = sdr.IIR.ZerosPoles(zeros, poles)

plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/60525a761a79396a63d77981a06b5cee243cdc8151c193277057096a3c8d42da.png

When the poles are complex conjugates and inside the unit circle, the impulse response \(h[n]\) is a decaying sinusoid.

zeros = []
pole = 0.8 * np.exp(1j * np.pi / 8)
poles = [pole, pole.conj()]
iir = sdr.IIR.ZerosPoles(zeros, poles)

plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/5fa977bd22976d68263c8ce37ba7abb2022061094c7f1e4309734cadddb42eda.png

When the pole is real and on the unit circle, the impulse response \(h[n]\) is constant. This filter is an integrator.

zeros = []
poles = [1]
iir = sdr.IIR.ZerosPoles(zeros, poles)

plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/49cb7e8483095b42c29caf599fac96aedcd7fbfb2948e6e0892513981dae7cac.png

When the poles are complex conjugates and on the unit circle, the impulse response \(h[n]\) is a sinusoid.

zeros = []
pole = 1 * np.exp(1j * np.pi / 8)
poles = [pole, pole.conj()]
iir = sdr.IIR.ZerosPoles(zeros, poles)

plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/4fa617083ddfc94151b41c2a57f9c95ab5b7096098bfe0ccf4ebec197831e1cd.png

When the pole is real and outside the unit circle, the impulse response \(h[n]\) is an exponential. This filter is unstable.

zeros = []
poles = [1.2]
iir = sdr.IIR.ZerosPoles(zeros, poles)

plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/23143fc6d1db6b1eb7f6d2a2b44307803f77371016ae4a703dd086f040f53a44.png

When the poles are complex conjugates and outside the unit circle, the impulse response \(h[n]\) is an exponentially-increasing sinusoid. This filter is unstable.

zeros = []
pole = 1.2 * np.exp(1j * np.pi / 8)
poles = [pole, pole.conj()]
iir = sdr.IIR.ZerosPoles(zeros, poles)

plt.figure(figsize=(10, 8))
sdr.plot.filter(iir.b_taps, iir.a_taps, N_time=30)
plt.show()
../../_images/4ee250f0649a4e5749ead75dad8f05728f24fcba72412ec8c76ece594e7135cd.png

Last update: Jul 15, 2023