Control_Theory_Summary
June 29, 2020
Walid El Bouhali
This Notebook contains all the important python commands that were used in the e-lectures
[1]: from control.matlab import *
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
[2]: # Laplace variable
s = tf('s')
# or
s = tf([1,0], [1])
s
[2]:
s
1
1 Transfer functions
[3]: # Tranfer functions can be made with the function tf
H = tf([1], [2, 1])
# which is the same as
H = 1/(1+2*s)
H
[3]:
1
2s + 1
[4]: # Transfer function for closed loop system
H1 = tf([1], [1, 2, 1])
H2 = tf([1], [1, 1, 1])
H1
[4]:
1
s2 + 2s + 1
1
[5]: H2
[5]:
1
s2 + s + 1
[6]: # which is the same as
H3 = H1 / (1 + H1 * H2)
H3
[6]:
s4 + 3s3 + 4s2 + 3s + 1
s6 + 5s5 + 11s4 + 14s3 + 12s2 + 7s + 2
[7]: # In this case use minreal() to remove cancelling poles
H3.minreal()
# H1.feedback(H2) does this automatically
[7]:
s2 + s + 1
s4 + 3s3 + 4s2 + 3s + 2
[8]: # The equivalent function will be
H3 = H1.feedback(H2)
H3
[8]:
s2 + s + 1
s4 + 3s3 + 4s2 + 3s + 2
[9]: # coefficients of numerator and denominator of a transfer function
H3.num[0][0], H3.den[0][0]
[9]: (array([1, 1, 1]), array([1, 3, 4, 3, 2]))
2 Signals
[10]: # time vector
dt = 0.01
t = np.arange(0, 20+dt, dt)
[11]: # sin function with frequency f
f1 = 0.1 # Hz
f2 = 2 # rad.s
y1 = np.sin(2 * np.pi * f1 * t) # if the freq is in Hz
y2 = np.sin(f2 * t) # if the freq is in rad/s
2
plt.plot(t, y1)
plt.plot(t, y2)
plt.show()
2.1 Example ramp & hold signal
[12]: # create a ramp and hold signal with the ramp starting at t = 0
# seconds, and finishing at t = 9[s] and with a ramp size of 4.8.
dt = 0.15
t = np.arange(0, 30+dt, dt)
rampend = 9 # end time of ramp phase
rampsize = 4.8 # size of the ramp
y = np.minimum(t/rampend, 1.)*rampsize
plt.plot(t, y)
plt.show()
3
2.2 Example staircase signal
[13]: t = np.arange(0, 0.5, 0.001)
u = t - np.mod(t, 0.1)
plt.plot(t,u)
plt.show()
4
3 Poles and Zeroes
[14]: # plot poles & zeroes using pzmap(H)
pzmap(H3)
plt.show()
5
[15]: # calculate the poles and zeroes of a transfer function
print("Poles: ", H3.pole(),"\n\nZeros: ", H3.zero())
Poles: [-1.35180795+0.72034174j -1.35180795-0.72034174j -0.14819205+0.91129216j
-0.14819205-0.91129216j]
Zeros: [-0.5+0.8660254j -0.5-0.8660254j]
4 Calculating step and impulse reponses
[16]: # step/impulse response response
t = np.arange(0, 30+dt, dt)
y1, t = step(H3, t, output=0) # if we have multiple output, you can also specify␣
,→which output you want
y2, t = impulse(H3, t)
plt.plot(t, y1, label= "step")
plt.plot(t, y2, label= "Impulse")
plt.title("Step / Impulse response")
plt.legend()
plt.show()
6
5 Calculating reponse to an arbitrary input
[17]: # we can use the function lsim to calculate the response to an input
# that is defined in a vector
# suppose our input is a ramp and hold signal and our transferfunction
# is H3, the response can be found by
dt = 0.15
t = np.arange(0, 30+dt, dt)
u = np.minimum(t/10, 1.)*3
y, t, x = lsim(H3, u, t) # note that lsim has 3 outputs (yout, t, xout)
plt.plot(t, y)
plt.show()
7
6 Calculating settling time, overshoot, rise time and delay time
[18]: # supposed we have the following system response
y, t = step(H3, t)
yend = y[-1]
tsettle = t[(y <= y[-1]*0.95) | (y >= y[-1]*1.05)][-1]
overshoot = np.max(y)/y[-1]*100 - 100
risetime = t[y >= y[-1]*0.9][0] - t[y <= y[-1]*0.1][-1]
delaytime = t[y <= y[-1]*0.1][-1]
print(tsettle, overshoot, risetime, delaytime)
14.399999999999999 37.22010210649461 1.3499999999999999 0.3
7 State-Space systems
ẋ (t) = Ax (t) + Bu(t)
y = Cx + Du
[19]: # we can enter the system above as follows
# parameter values
m = 3.5
b = 9
8
k = 60
# matrices
A = np.array([[ 0, 1], [ -k/m, -b/m]])
B = np.array([[ 0], [ 1/m]])
C = np.array([[ 1, 0]])
D = np.array([[ 0]])
# system
sys = ss(A, B, C, D)
7.1 Response to an initial condition of a state-space system
[20]: # initial condition y(0) = 0 and y'(0) = 1
x0 = np.array([[0], [1]])
# response
dt = 0.01
t = np.arange(0, 5+dt, dt)
y, t = initial(sys, t, x0)
# plot
plt.plot(t, y)
plt.hlines(0, 0, 5)
plt.show()
9
7.2 Response to a sin function as input to a state-space system
[21]: # supposed we have as input a sin function with frequency w
w = 3.9357078 # rad/s
# remember that if the frequency is in rad/s our sin function looks like
t = np.arange(0.0, 20.01, 0.01)
u = np.sin(w * t)
# Now we use lsim
y , t, x = lsim(sys, u, t)
plt.plot(t, y)
plt.show()
7.3 Converting a state-space system to transfer functions
[22]: # consider the system
A = np.array([ [ -1.7, 0, 0 ],
[ 0.4, -1.2, -5.0],
[ 0, 1, 0 ] ])
B = np.array([ [0.3 ], [0 ], [0 ] ])
C = np.array([ [0, 1, 0 ],
[0.1, 0, 1 ]])
D= np.array([ [0],
[0] ])
10
sys = ss(A, B, C, D)
# we can then simply convert this state-space system to a system
# of transfer functions
H = tf(sys)
H
[22]:
" #
0.12s−1.943e−16
s3 +2.9s2 +7.04s+8.5
0.03s2 +0.036s+0.27
s3 +2.9s2 +7.04s+8.5
[23]: # we can then find the poles of the transfer functions by
H.pole()
[23]: array([-0.60000006+2.15406594j, -0.60000006-2.15406594j,
-0.59999994+2.15406591j, -0.59999994-2.15406591j,
-1.70000011+0.j , -1.69999989+0.j ])
7.4 Converting a MIMO system to multiple separate transfer functions
Consider the following MIMO system
[24]: #constants
Jb = 400
Jp = 1000
k = 10
b = 5
s = tf([1,0], [1])
#transferfunctions
hb1 = 1/(Jb*s)
hb2 = 1/s
hp1 = 1/(Jp*s)
hp2 = 1/s
#systems
sys1 = ss(hb1)
sys2 = ss(hb2)
sys3 = k
sys4 = ss(hp1)
sys5 = tf(hp2)
sys6 = b
sys = append(sys1, sys2, sys3, sys4, sys5, sys6)
11
Q = np.array([[1, -3, -6], # u1 = -y3 - y6
[2, 1, 0], # u2 = y2
[3, 2, -5], # u3 = y2 - y5
[4, 3, 6], # u4 = y3 + y6
[5, 4, 0], # u5 = y4
[6, 1, -4]]) # u6 = y1 - y4
inputs = [1]
outputs = [1, 2, 4, 5]
syscon = connect(sys, Q, inputs, outputs)
tf(syscon)
[24]:
0.0025s3 +1.25e−05s2 +2.5e−05s
s 4 +0.0175s3 +0.035s2 −1.626e −19s
0.0025s2 +1.25e−05s+2.5e−05
4
s +0.0175s3 +0.035s 2 +5.692e −19s −2.168e −19
1.25e−05s2 +2.5e−05s+2.844e−22
s4 +0.0175s3 +0.035s2 +8.132e−20s−2.752e−19
1.25e−05s+2.5e−05
s4 +0.0175s3 +0.035s2 −1.138e−18s−3.253e−19
Now, let’s say we want these transfer functions separately
[25]: # We can access each TF seperately as follows:
H1 = tf([1, 0, 0, 0] * syscon)
H2 = tf([0, 1, 0, 0] * syscon)
H3 = tf([0, 0, 1, 0] * syscon)
H4 = tf([0, 0, 0, 1] * syscon)
H1
[25]:
0.0025s2 + 1.25e − 05s + 2.5e − 05
s3 + 0.0175s2 + 0.035s − 1.626e − 19
7.5 Converting a transfer function to a state-space system
Take the following transfer function, and convert it into a state-space system.
The state-space system should have two outputs, one is the output of the transfer
function, and the second output is the derivative of the first output.
b0 +b1 s+b2 s2
H (s) = a0 + a1 s + a2 s2 + a3 s3
[26]: # coefficients
b0, b1, b2 = 1.0, 0.1, 0.5
a0, a1, a2, a3 = 2.3, 6.3, 3.6 , 1.0
h = tf([b2, b1, b0], [a3, a2, a1, a0])
12
H = tf([[h.num[0][0]], [(h*s).num[0][0]]], # h*s
[[h.den[0][0]], [(h*s).den[0][0]]])
H
[26]:
0.5s2 +0.1s+1
" #
s3 +3.6s2 +6.3s+2.3
0.5s3 +0.1s2 +s
s3 +3.6s2 +6.3s+2.3
[27]: # convert from set of transfer functions to state-space system
sys = ss(H)
sys
[27]: A = [[-3.60000000e+00 -6.30000000e+00 2.30000000e+00]
[ 1.00000000e+00 9.16269055e-16 -4.85857436e-17]
[ 0.00000000e+00 -1.00000000e+00 -1.42078581e-16]]
B = [[1.]
[0.]
[0.]]
C = [[ 0.5 0.1 -1. ]
[-1.7 -2.15 1.15]]
D = [[0. ]
[0.5]]
[28]: # as verification, you can check that the A matrix eigenvalues
# are equa; to the transfer funtion poles
from scipy.linalg import eig
print(eig(sys.A)[0])
print(h.pole())
[-1.56072802+1.53960194j -1.56072802-1.53960194j -0.47854395+0.j ]
[-1.56072802+1.53960194j -1.56072802-1.53960194j -0.47854395+0.j ]
13
7.6 Converting a block diagram to a state-space system using “append and connect”
Consider the block diagrma above; ui and yi represent the input and output to each
block, respectively. Each block is defined as a system. We will convert this into
a state-space system using append and connect.
[29]: # Define s variable
s=tf([1,0],[1])
# fill in variables
K1 = 1
K2 = 2.6
K3 = 2.5
K4 = 0.6
# Define systems; the appended system type will the type of the first system
# given. If the following systems do not have a proper form they will be
# transformed automatically
# Sys1 of the block diagram is a constant which has to be transformed into a
# state-space system.
sys1 = ss(K4 * (s/s)) # Multiply with s/s to 'trick' the control module into␣
,→thinking K4 is a transfer function
sys2 = ss(1/s)
sys3 = ss(1/(s+1))
sys4 = ss(1/s)
sys5 = K1 # Will be transformed by the append function
sys6 = K2
sys7 = K3
14
sys8 = 1 # Add dummy sys8 in order to sum y4 and y5
sys = append(sys1,sys2,sys3,sys4,sys5,sys6,sys7,sys8)
Q = np.array([[1,-6,-7], # u1 = -y6 - y7
[2, 1, 0], # u2 = y1
[3, 2, 0], # u3 = y2
[4, 3, 0], # u4 = y3
[5, 2, 0], # u5 = y2
[6, 3, 0], # u6 = y3
[7, 4, 0], # u7 = y4
[8, 4, 5]]) # u8 = y4 + y5
inputs = [1,2] # R(s) input of sys1, D(s) input of sys2
outputs = [8] # Y(s) output of sys8
syscon = connect(sys, Q, inputs, outputs)
print(syscon)
A = [[ 0. -1.56 -1.5 ]
[ 1. -1. 0. ]
[ 0. 1. 0. ]]
B = [[0.6 1. ]
[0. 0. ]
[0. 0. ]]
C = [[1. 0. 1.]]
D = [[0. 0.]]
[30]: # Solving it by hand leads to the same result
# fill in variables
K1 = 1
K2 = 2.6
K3 = 2.5
K4 = 0.6
A = np.matrix([[ 0.0, -K4*K2, -K4*K3],
[ 1, -1 , 0],
[ 0, 1 , 0]])
B = np.matrix([[ K4, 1.0],
[ 0, 0],
[ 0, 0]])
C = np.matrix([[K1, 0.0, 1]])
15
D = np.matrix([[0, 0.0]])
# and create the system
sys = ss(A, B, C, D)
sys
[30]: A = [[ 0. -1.56 -1.5 ]
[ 1. -1. 0. ]
[ 0. 1. 0. ]]
B = [[0.6 1. ]
[0. 0. ]
[0. 0. ]]
C = [[1. 0. 1.]]
D = [[0. 0.]]
7.7 Controllers for state-space systems
Consider the system above. If we want to add a controller to our original
state-space system we can do so with the feedback function.
[31]: # Original ss system
A = np.array([[-0.2, 0.06, 0, -1],
[0, 0, 1, 0],
[-17, 0, -3.8, 1],
16
[9.4, 0, -0.4, -0.6]])
B = np.array([[-0.01, 0.06],
[0, 0],
[-32, 5.4],
[2.6, -7]])
C = np.eye(4)
D = np.zeros((4, 2))
sys1 = ss(A, B, C, D)
# feedback gain
Kd = -0.55
K = np.zeros((2,4))
K[-1, -1] = K1
# New ss system
sys2 = sys1.feedback(K)
sys2
[31]: A = [[ -0.2 0.06 0. -1.06]
[ 0. 0. 1. 0. ]
[-17. 0. -3.8 -4.4 ]
[ 9.4 0. -0.4 6.4 ]]
B = [[-1.0e-02 6.0e-02]
[ 0.0e+00 0.0e+00]
[-3.2e+01 5.4e+00]
[ 2.6e+00 -7.0e+00]]
C = [[1. 0. 0. 0.]
[0. 1. 0. 0.]
[0. 0. 1. 0.]
[0. 0. 0. 1.]]
D = [[0. 0.]
[0. 0.]
[0. 0.]
[0. 0.]]
[32]: # We can also find the damping coefficients and frequency
damp(sys1)
_____Eigenvalue______ Damping___ Frequency_
-4.074 1 4.074
-0.2625 +3.278j 0.07983 3.289
-0.2625 -3.278j 0.07983 3.289
-0.001089 1 0.001089
17
[32]: (array([4.07381994e+00, 3.28874827e+00, 3.28874827e+00, 1.08937685e-03]),
array([1. , 0.07983139, 0.07983139, 1. ]),
array([-4.07381994e+00+0.j , -2.62545342e-01+3.27825184j,
-2.62545342e-01-3.27825184j, -1.08937685e-03+0.j ]))
8 Gain Tuning with Root-Locus
Remember that the number of asymptotic directions is equal to the number of poles -
number of zeros
The asymptotes intersect the real axis at the centroid x a and leave at an angle φa ,
given by:
8.1 Root locus example
Consider the following system. Use the root-locus method to find a gain Kθ needed
to get the pole in the origin moved to -0.4.
with
[33]: # consider the following system
Kq = -24
ksi = 2/np.sqrt(13)
18
w = np.sqrt(13)
T = 1.4
H = Kq * (1 + T*s) / ( s * (s**2 + 2 * ksi * w * s + w**2))
K0 = 1 # first we assume the gain to be 1
Heq = (K0 * H) # conside ONLY the feed forward path
sisotool(-Heq) # note that we take -Heq, will later correct for that
Now simply click on the blue line to move the purple square to a value of -0.4,
then read off the value for the gain, in this case 0.438. Now we correct for the
minus sign (sisotool(-Heq)) and thus Kθ = −0.438
To find the damping coefficient we can either click on one of the purple squares
for the imaginary poles and read of the value for ζ sp , or we fill in our value for
Kθ and use the damp function.
[34]: K0 = -0.438
Heq = (K0 * H).feedback(1) # NOW WE TAKE THE FEEDBACK LOOP! Since we want to␣
,→know the damping coefficient of the entire system
damp(Heq)
_____Eigenvalue______ Damping___ Frequency_
19
-1.8 +4.8j 0.3511 5.126
-1.8 -4.8j 0.3511 5.126
-0.4001 1 0.4001
[34]: (array([5.12607415, 5.12607415, 0.40005121]),
array([0.35114092, 0.35114092, 1. ]),
array([-1.7999744 +4.79965919j, -1.7999744 -4.79965919j,
-0.40005121+0.j ]))
Which gives us a damping coefficient of ζ sp = 0.3511
9 Bode
$ $To convert the magnitude in decibels Ldb to | H(jω) | (unitless):
| H ( jω )| = 10Ldb /20
To convert from | H(jω) | to decibels:
Ldb = 20 · log (| H ( jω )|)
[35]: # The following functions help to find phase and gain margins as well as other␣
,→useful parameters
# (taking the same H as before)
gm, pm, wg, wp = margin(Heq) # gain margin, phase margin and respective omega␣
,→values ## NON dB
mag, phase, omega = bode(Heq) # Plots + lists for magnitude, phase and omega ##␣
,→NON dB
print("Resonance peak in db =", 20*np.log10(mag.max())) #prints the maximum␣
,→magnitude
gm, pm
Resonance peak in db = -0.0007253064572176859
[35]: (inf, inf)
20
[36]: # To use the bode plot use
bode([Heq], omega=np.logspace(-5,1,500)) # adjust the range and # of steps as␣
,→pleased
# or
sisotool(Heq, omega=np.logspace(-5,1,500), Hz= False) # last argument do get␣
,→horixzontal axis in rad/s
21
9.1 Reading plots: Stability, Mimimum phase and Type N systems
Stability of a transfer function or a system can be assessed by looking at its
pzmap and its bode plots.
For a stable system, the poles all have a real part smaller or equal to 0:
Re( pi ) ≤ 0
22
In this case, the system is stable. In addition we can see that one zero has a
positive real part. This implies that the system is non minimim phase. In other
words, there will be some negative overshoot with respect to the final response of
the system.
A system achieves minimum phase when: (zeros = z)
Re(z j ) ≤ 0
For a polar plot, stability is assessed by checking where the bode line crosses the
real axis. Indeed, at
Re( H ( jω )) ≤ 0
when
Im( H ( jω )) = 0
then the transfer function will generate an unstable response.
The type of system can be determined by observing the number of poles in the
origin. A Type 1 system will have 1 pole in the origin implying its transfer
23
function will contain a
1/s
term. This will be displayed in the bode plot as a -90 degree downward shift for
the whole spectrum.
You can determine the system type by looking at the phase when omega = 0. If for
instance the phase = 0 when omega = 0 then the phase bode plot will display an
initial phase of 0. This implies the lack of any 1/sˆn terms meaning there will be
no poles in the origin and the system is thus a Type 0 system.
9.2 Contructing and decoding a bode plot: tf Bode
The figure below gives you all the tools to identify the shape of a transfer
function from a bode plot
24
Good luck!
[ ]:
25