3. Frequency response plots

Frequency responses are very easy to calculate numerically if we remember that the frequency domain is basically the part of the Laplace domain on the imaginary axis, or mathematically \(s=i\omega\)

[1]:
import numpy
import matplotlib.pyplot as plt
%matplotlib inline

Frequency responses often make use of logarithmic scales, so we’ll generate logaritmically spaced points. This will be linearly spaced on a logarithmic scale:

[2]:
omega = numpy.logspace(-2, 2, 1000)
[3]:
s = omega*1j

It is important to realise that omega and s are just collections of numbers:

[4]:
omega[:5]
[4]:
array([0.01      , 0.01009262, 0.0101861 , 0.01028045, 0.01037567])
[5]:
s[:5]
[5]:
array([0.+0.01j      , 0.+0.01009262j, 0.+0.0101861j , 0.+0.01028045j,
       0.+0.01037567j])

As an example, we can use a first order transfer function

[6]:
tau1 = 2
[7]:
G1 = 1/(tau1*s + 1)

and a second order with complex poles

[8]:
tau = 1
[9]:
zeta = 0.5
[10]:
G2 = 1/(tau**2*s**2 + 2*tau*zeta*s + 1)

Similarly to omega and s, G1 and G2 are just arrays.

[11]:
G1[:5]
[11]:
array([0.99960016-0.019992j  , 0.99959272-0.02017702j,
       0.99958515-0.02036375j, 0.99957743-0.02055221j,
       0.99956957-0.0207424j ])
[12]:
G2[:5]
[12]:
array([0.99999999-0.010001j  , 0.99999999-0.01009365j,
       0.99999999-0.01018716j, 0.99999999-0.01028153j,
       0.99999999-0.01037678j])

4. Bode

Bode diagrams are the most common plots. The magnitude and angle of the frequency response is shown as a function of frequency. This is such a common representation that when most control engineers say something like “Show me the Frequency response” they will mean “Show me a Bode diagram”.

[13]:
def bode(G):
    fig, [ax_mag, ax_phase] = plt.subplots(2, 1)
    ax_mag.loglog(omega, numpy.abs(G))
    ax_phase.semilogx(omega, numpy.angle(G))
[14]:
bode(G1)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_20_0.png
[15]:
bode(G2)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_21_0.png

It is easy to predict how the product of the two functions would look since both plots add together when the transfer functions are multiplied.

[16]:
bode(G1*G2)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_23_0.png

5. Phase unwrapping

Wait… What happened in that plot? That discontinuity happens when the angle reaches \(-\pi\) and starts counting positive angles, jumping to \(\pi\). To get rid of this, we need to redefine our Bode function to use the function numpy.unwrap which removes discontinuities by subtracting an appropriate multiple of \(2\pi\).

[17]:
def bode(G):
    fig, [ax_mag, ax_phase] = plt.subplots(2, 1)
    ax_mag.loglog(omega, numpy.abs(G))
    ax_phase.semilogx(omega, numpy.unwrap(numpy.angle(G)))
[18]:
bode(G1*G2)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_26_0.png

Much better.

6. Nyquist

Nyquist diagrams are simply the real and imaginary components of the frequency response plotted on a plane. By convention not only the positive frequencies are plotted but the negative as well. This is the image of \(G(s)\) as \(s\) traverses the entire imaginary line.

[19]:
def nyquist(G):
    plt.plot(G.real, G.imag,
             G.real, -G.imag)
    plt.xlabel('Real')
    plt.ylabel('Imag')
    plt.axis('equal')
[20]:
nyquist(G1)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_30_0.png
[21]:
nyquist(G2)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_31_0.png
[22]:
nyquist(G1*G2)
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_32_0.png

You can see an extra 90° twist for every order below the line.

7. With the control library

The control library saves us some typing to create these diagrams:

[23]:
import control
[24]:
G = control.tf(1, [tau**2, 2*tau*zeta, 1])
[25]:
control.bode(G, omega);
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_37_0.png
[26]:
control.nyquist_plot(G, omega);
../../_images/1_Dynamics_8_Frequency_domain_Frequency_response_plots_38_0.png
[ ]: