12.3. Transfer functions#

When we previously analyzed FIR filters, it became useful to examine the discrete Fourier transform \(\red{H[m]}\) of the impulse response \(\red{h[k]}\). The frequency domain view of convolutional filters immediately exposes how a filter will delay and gain (or attenuate) different frequencies. However, as noted earlier in this chapter, the DFT cannot be applied directly to feedback systems with infinite impulse responses, and this led to our derivation of the z-Transform.

In this section, we’ll return to our initial motivation, and derive something similar to \(\red{H[m]}\), except from the perspective of the z-Transform rather than the DFT. The resulting object is known as the transfer function of a filter, and is denoted by \(\red{H(z)}\).

12.3.1. Defining a transfer function#

Let’s start with a linear IIR filter in standard form:

\[\cyan{a[0]} \cdot \purple{y[n]} = \sum_{k=0}^{K-1} \red{b[k]} \cdot \blue{x[n-k]} - \sum_{k=1}^{K-1} \cyan{a[k]} \cdot \purple{y[n-k]},\]

where \(\red{b[k]}\) and \(\cyan{a[k]}\) denote the feed-forward and feed-back coefficients of the filter.

If we move all feedback terms to the left-hand side of the equation, we obtain an equivalent equation:

(12.5)#\[\sum_{k=0}^{K-1} \cyan{a[k]} \cdot \purple{y[n-k]} = \sum_{k=0}^{K-1} \red{b[k]} \cdot \blue{x[n-k]}.\]

While this form is not useful for computing \(\purple{y}\), it is useful for analyzing \(\purple{y}\)!

In particular, you might recognize that both sides of the equation are convolutions:

(12.6)#\[\cyan{a}*\purple{y} = \red{b}*\blue{x}.\]

This means that if we take the z-transform of both sides, we can use the z-transform convolution theorem:

\[\cyan{A(z)} \cdot \magenta{Y(z)} = \red{B(z)} \cdot \darkblue{X(z)},\]

where \(\cyan{A(z)}, \magenta{Y(z)}, \red{B(z)}, \darkblue{X(z)}\) denote the z-transforms of \(\cyan{a}\), \(\purple{y}\), \(\red{b}\), and \(\blue{x}\) respectively.

As long as \(\cyan{A(z)}\) is not zero — and it generally is non-zero except for at most \(K\) specific choices of \(\purple{z}\) — we can divide through to isolate \(\magenta{Y(z)}\):

\[\magenta{Y(z)} = \frac{\red{B(z)}}{\cyan{A(z)}} \cdot \darkblue{X(z)}.\]

This gives us something highly reminiscent of the convolution theorem: filtering in the time domain has again be expressed as multiplication, except now in the \(z\)-plane instead of the frequency domain.

Definition 12.2 (Transfer function)

Let \(\cyan{a}\) and \(\red{b}\) denote the feed-back and feed-forward coefficients of a linear IIR filter:

\[\cyan{a[0]} \cdot \purple{y[n]} = \sum_{k=0}^{K-1} \red{b[k]} \cdot \blue{x[n-k]} - \sum_{k=1}^{K-1} \cyan{a[k]} \cdot \purple{y[n-k]}.\]

The transfer function of this filter is defined as follows

(12.7)#\[\red{H(z)} = \frac{\red{B(z)}}{\cyan{A(z)}} = \frac{\displaystyle\sum_{k=0}^{K-1} \red{b[k]} \cdot z^{-k}}{\displaystyle\sum_{k=0}^{K-1} \cyan{a[k]} \cdot z^{-k}}\]

12.3.2. Using transfer functions#

As mentioned previously, transfer functions can be thought of as providing a generalization of the convolution theorem to support feedback filters. Specifically, we have

(12.8)#\[\magenta{Y(z)} = \red{H(z)} \cdot \darkblue{X(z)} =\frac{\red{B(z)}}{\cyan{A(z)}} \cdot \darkblue{X(z)}.\]

Note that as a special case, if \(\cyan{a = [1]}\) (so that there is no feedback in the filter), we get the z-transform

\[\cyan{A(z)} = \sum_{k=0}^\infty \cyan{a[k]} \cdot z^{-k}= \cyan{a[0]} \cdot z^{0} = \cyan{a[0]} = 1.\]

In this case, the transfer function simplifies to \(\red{H(z)} = \red{B(z)} / \cyan{1} = \red{B(z)}\), and we recover the convolution theorem.

More generally, we can still reason about \(\red{H(z)}\) as the object that transforms the input signal \(\blue{x}\) into the output signal \(\purple{y}\). Evaluating \(\red{H(z)}\) at values of \(\purple{z}\) with unit magnitude — i.e. \(\purple{z= e^{\mathrm{j}\cdot \theta}}\) — produces the frequency response of the filter. In Python, this is provided by the function scipy.signal.freqz (frequency response via z-transform).

12.3.2.1. Example: analyzing a Butterworth filter#

In Using IIR filters, we constructed a Butterworth filter and rather crudely analyzed its effect on an impulse input by truncating the output \(y\) and taking the DFT. Let’s revisit this example, but instead analyze it using the z-transform and transfer functions.

fs = 44100  # 44.1K sampling rate
fc = 500    # 500 Hz cutoff frequency
order = 10  # order-10 filter

b, a = scipy.signal.butter(order, fc, fs=fs)

freq, H = scipy.signal.freqz(b, a, fs=fs)

The result of this computation is an array freq of frequencies (spaced uniformly between 0 and fs/2), and an array H that evaluates the transfer function at each specified frequency.

The figure below illustrates the response curve by plotting freq on the horizontal axis and abs(h) (in decibel scale) on the vertical axis, i.e.:

import matplotlib.pyplot as plt

# Convert |H| to decibels, with a -120dB noise floor
H_dB = 20 * np.log10(np.abs(H) + 1e-6)
plt.plot(freq, H_dB)
Frequency response curve of a Butterworth filter

Fig. 12.3 The frequency response curve \(|\red{H(z)}|\) for an order-10 Butterworth filter with a cutoff frequency at \(f_c=500\) Hz and sampling rate \(f_s=44100\).#

Note that we never had to construct a test signal to generate this curve: all of the information was inferred from the filter coefficients b and a!

12.3.2.2. Example: phase response of an elliptic filter#

Just like with convolutional filters, we can infer delay properties of IIR filters by looking at the phase spectrum contained in \(\red{H(z)}\). This can be done directly, e.g., by calling scipy.signal.freqz and then using np.unwrap(np.angle(h)) to compute the unwrapped phase. Alternatively, it is usually more convenient to use the already provided group_delay function, as demonstrated below for an elliptic filter.

fs = 44100  # Sampling rate
fc = 500  # Cutoff frequency
fstop = 600
attenuation = 40  # we'll require 80 dB attenuation in the stop band
ripple = 3  # we'll allow 3 dB ripple in the passband

order_ell, wn = scipy.signal.ellipord(fc, fstop, ripple, attenuation, fs=fs)
b_ell, a_ell = scipy.signal.ellip(order_ell, ripple, attenuation, wn, fs=fs)

# scipy's group delay measures in samples
# we'll convert to seconds by dividing out the sampling rate
freq, delay = scipy.signal.group_delay([b_ell, a_ell], fs=fs)
delay_sec = delay / fs

Note that scipy.signal.group_delay returns the delay for each measured frequency in freq in units of [samples]. To convert the delay measurements to [seconds], we divide by the sampling rate fs. A visualization of the resulting group delay (along with the frequency response) is provided below.

Frequency and phase response curves of an Elliptic filter

Fig. 12.4 The frequency response curve \(|\red{H(z)}|\) (top) and phase response curve (group delay, bottom) for an elliptic filter with cutoff \(f_c=500\), transition bandwidth of 100 Hz, 3dB passband ripple, and 40dB stop-band attenuation (\(f_s=44100\)). The plot is limited to the frequency range [0, 2000] so that the trends within the passband are easier to see.#

12.3.3. Why not use the DTFT?#

Everything we’ve done so far only depends on \(z\) with unit magnitude: frequency response and phase response are both properties of \(\red{H\left(e^{j\cdot \theta}\right)}\), which we can think of as computing the discrete-time Fourier transform (DTFT). At this point, it is completely reasonable to wonder why we needed to define the z-transform to support any complex number \(\purple{z}\), instead of just those points on the unit circle.

As we will see in the next section, expanding the definition to include all complex \(z\) allows us to study the stability of a filter, and this would not be possible with just the DTFT.