Main

Stability Analysis

Stability analysis is finding the range of controller gains that lead to a stabilizing controller. There are multiple methods to compute this range between a lower limit `K_{cL}` and an upper limit `K_{cL}`.

$$K_{cL} \le K_c \le K_{cU}$$

This range is important to know the range of tuning values that will not lead to a destabilizing controller.

Exercise

Consider a feedback control system that has the following open loop transfer function.

$$G(s) = \frac{4K_c}{(s+1)(s+2)(s+3)}$$

Determine the values of `K_c` that keep the closed loop system response stable.

Solution

Root Locus Plot

Determine where the real portion of the roots crosses to the right hand side of the plane. In this case, the real part of two roots becomes positive at `K_c=15`.

Bode Plot

Determine the gain margin at -180o phase. The magnitude at -180o phase is about -23 dB. With `-23 = 20 log_{10} (AR)`, the gain margin is `1/{AR}` and approximately equal to 15. This is the upper bound on the controller gain to keep the system stable. This answer agrees with the root locus plot solution.

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

# open loop
num = [4.0]
den = [1.0,6.0,11.0,6.0]
sys = signal.TransferFunction(num, den)
t1,y1 = signal.step(sys)

# closed loop
Kc = 1.0
num = [4.0*Kc]
den = [1.0,6.0,11.0,4.0*Kc+6.0]
sys2 = signal.TransferFunction(num, den)
t2,y2 = signal.step(sys2)

plt.figure(1)
plt.subplot(2,1,1)
plt.plot(t1,y1,'k-')
plt.legend(['Open Loop'],loc='best')

plt.subplot(2,1,2)
plt.plot(t2,y2,'r--')
plt.legend(['Closed Loop'],loc='best')
plt.xlabel('Time')

# root locus plot
import numpy.polynomial.polynomial as poly
n = 1000  # number of points to plot
nr = 3    # number of roots
rs = np.zeros((n,2*nr))   # store results
Kc = np.logspace(-2,2,n)  # Kc values
for i in range(n):        # cycle through n times
    den = [1.0,6.0,11.0,4.0*Kc[i]+6.0] # polynomial
    roots = poly.polyroots(den) # find roots
    for j in range(nr):   # store roots
        rs[i,j] = roots[j].real # store real
        rs[i,j+nr] = roots[j].imag # store imaginary
plt.figure(2)
plt.subplot(2,1,1)
plt.xlabel('Root (real)')
plt.ylabel('Root (imag)')
plt.grid(b=True, which='major', color='b', linestyle='-')
plt.grid(b=True, which='minor', color='r', linestyle='--')
for i in range(nr):
    plt.plot(rs[:,i],rs[:,i+nr],'.')
plt.subplot(2,1,2)
plt.plot([Kc[0],Kc[-1]],[0,0],'k-')
for i in range(3):
    plt.plot(Kc,rs[:,i],'.')
plt.ylabel('Root (real part)')
plt.xlabel('Controller Gain (Kc)')

# bode plot
w,mag,phase = signal.bode(sys)
plt.figure(3)
plt.subplot(2,1,1)
plt.semilogx(w,mag,'k-',linewidth=3)
plt.grid(b=True, which='major', color='b', linestyle='-')
plt.grid(b=True, which='minor', color='r', linestyle='--')
plt.ylabel('Magnitude')

plt.subplot(2,1,2)
plt.semilogx(w,phase,'k-',linewidth=3)
plt.grid(b=True, which='major', color='b', linestyle='-')
plt.grid(b=True, which='minor', color='r', linestyle='--')
plt.ylabel('Phase')
plt.xlabel('Frequency')

plt.show()