Skip to content

Commit 44067bc

Browse files
committed
Attempted loop shaping
1 parent 1a4542e commit 44067bc

4 files changed

Lines changed: 185 additions & 21 deletions

File tree

SysReg_crashcourse_FreqDomain_BlockB.py

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -940,12 +940,16 @@ def animate(t):
940940
#
941941
# | Frequencies | $C(s)$ | $\mid S(s)\mid$ |
942942
# | -------- | ------- | -------- |
943-
# | Low | - | Low gain ensures disturbance rejection |
943+
# | Low | High gain ensures unscaled tracking | Low gain ensures disturbance rejection |
944944
# | Cross-over | Ensure good GM and PM | Ensure good SM |
945945
# | High | Low gain ensures measurement noise rejection | - |
946946
#
947+
# That unscaled tracking cell confused me slightly, so briefly why: the transfer function $G_{yr} = TF = \frac{L}{1+L}F$. Ignoring $F$ for a second, $\lim_{|L|\rightarrow\infty}\frac{L}{1+L} = 1$, meaning there
948+
# is unitary gain for constant input, i.e. unscaled reference tracking.
949+
#
947950
# ## Easier said...
948-
# So lets run an example and you'll understand better (I hope). We start with a unitary feedback controller and see how we're doing and what plant we have.
951+
# So lets run an example and you'll understand better (I hope). This will be very much a Plato-style of explanation (coincidentally my favourite style). We start with a unitary
952+
# feedback controller and see what plant we're dealing with.
949953
#
950954
# %%
951955
%matplotlib notebook
@@ -973,18 +977,99 @@ def animate(t):
973977

974978
###############################################
975979
SYS = loopShaper()
976-
fig = plt.figure(figsize=[15, 6])
977-
gs = GridSpec(3,2, figure=fig)
978-
ax = [fig.add_subplot(a) for a in [gs[0,0], gs[1, 0], gs[2, 0], gs[:, 1]]]
980+
fig = plt.figure(figsize=[15, 8])
981+
gs = GridSpec(4,2, figure=fig)
982+
ax = [fig.add_subplot(a) for a in [gs[0,0], gs[1, 0], gs[2, 0], gs[:3, 1], gs[3,:]]]
983+
984+
SYS.plot_LS(ax)
985+
display(fig)
986+
987+
# %% [markdown]
988+
# What should you get from this plot? Well, lets start with the crossover frequencies: they're around 30 or 300 rad/s. The loop transfers below and above have a low gain. Our margins are looking good however
989+
# and we can gain some performance for sure. Lets work low to high frequencies. For the lower frequencies our sensitivity needs low gains, so our loop needs high gains, also ensuring good tracking.
990+
# It doesn't, so lets add an integrator:
991+
992+
# %%
993+
SYS.Cpoles = [0]
994+
SYS.OM = np.logspace(-5, 5, 1000)
979995

996+
[a.cla() for a in ax]
980997
SYS.plot_LS(ax)
981998
display(fig)
982999

1000+
# %% [markdown]
1001+
# We need a bit more downslope around 1E-3 rad/s, i.e. more poles:
1002+
1003+
# %%
1004+
SYS.Cpoles = [0, -1e-3]
1005+
1006+
[a.cla() for a in ax]
1007+
SYS.plot_LS(ax)
1008+
display(fig)
1009+
1010+
# %% [markdown]
1011+
# Okaayyy, lower frequencies looking good. Zooming in on the crossover frequency next:
1012+
1013+
# %%
1014+
[a.set_xlim([1e-2, 1e2]) for a in ax[:3]]
1015+
ax[0].set_ylim([1e-3, 5e1])
1016+
display(fig)
1017+
1018+
# %% [markdown]
1019+
# Notes: our PM is very large, as well as our GM. In this case we can decrease both of these with the gain. Lets aim for $35^\circ$ PM, looking at the phase plot that happens at 50 or so rad/s, the
1020+
# magnitude there now is $5E-4$. *Therefore*, our gain can be $2E3$:
1021+
1022+
# %%
1023+
SYS.Cgain = 2e2
1024+
1025+
[a.cla() for a in ax]
1026+
SYS.plot_LS(ax)
1027+
display(fig, np.pi/3)
1028+
1029+
# %% [markdown]
1030+
# Honestly I suck at loop shaping.
1031+
#
1032+
# ## Feedforward control
1033+
# We've been ignoring the feedforward block up to now, but really it's very powerful when you can measure the output noise.
1034+
# Consider when there's no input noise $d$, which is often the case, and we add an extra feedforward control $u_\text{ff}$
1035+
# such that $u = Ce + u_\text{ff}$. Now append/recall $y = TFr + PSu_\text{ff} + Sn$
1036+
# Then with the measurement of the output noise $n$, define $u_\text{ff} = P^{-1}Fr - P^{-1}n$
1037+
# $$ y = (1- S)Fr + PSu_\text{ff} + Sn = Fr - SFr + PS(P^{-1}Fr - P^{-1}) + Sn = Fr - SFr + PSP^{-1}Fr - PSP^{-1}n + Sn $$
1038+
# $$ = Fr - SFr + SFr - Sn + Sn = Fr.$$
1039+
# This means you have perfect tracking. However, inverting the plant might not always be possible (non minimum phase systems for example).
1040+
# There are more tricks like this when you have knowledge of your disturbances.
9831041

9841042
# %% [markdown]
9851043
# # Fundamental Limitations
986-
# I'm so sorry, but everything we've done is technically speaking bachelor level control engineering. Now that we're nearing the end of the course content, we run into problems that we ignored before.
1044+
# ## Waterbed effect
1045+
# The funny thing about control is, the most important proofs are the proofs that say that something
1046+
# is **impossible**. These are the bounds you cannot circumvent. Bode's integral formula, otherwise known as the waterbed effedct,
1047+
# is one of these. It is defined as: assume L has relative degree $geq$ 2 and $N_p$ RHP-poles $p_i$, then
1048+
# $$ \int_0^\infty\log|S(i\omega)|\text{d}\omega = \pi\sum_{i=1}^{N_p} \mathfrak{Re}(p_i).$$
1049+
# This is somewhat reminiscent of the Nyquist plot. This integral is basically the integral of the vector going from -1 to the Nyquist plot.
1050+
# Iff this integral makes a half ellips on the onesided Nyquist contour, there is an encirclement, there is a RHP-pole.
1051+
#
1052+
# What it's also saying is: you can never change the value of that integral (assuming you don't add RHP poles). Therefore, if you change C
1053+
# to take some magnitude in S away, it has to come back at other frequencies.
1054+
#
1055+
# ## Time delays
1056+
# Time delays are annoying, because they put a hard limit on your bandwidth. Why? Well, a time delay $e^{-\theta s}$, has an idealized $T(s)=e^{-\theta s}$.
1057+
# The bandwidth is defined as $|T(i\omega_\text{B})| = \frac{1}{\sqrt2}$. Also
1058+
# $$|S + T| = 1 \leq |S| + |T| \rightarrow |S(i\omega_\text{B})| + \frac{1}{\sqrt2} \geq 1 \rightarrow |S(i\omega_\text{B})| \geq 1 - \frac12\sqrt2$$
1059+
# $$ S = 1 - e^{-\theta s} = 1 - \cos(-\theta\omega) - i\sin(-\theta\omega) \rightarrow |S| = \sqrt{ (1 - \cos(-\theta\omega)^2 + \sin(-\theta\omega)^2}$$
1060+
# $$ |S| = 1 \rightarrow (1 - \cos(-\theta\omega)^2 + \sin(-\theta\omega)^2 = 1$$
1061+
# $$ (1 - \cos(-\theta\omega)^2 + \sin(-\theta\omega)^2 = 1 = 1 - 2\cos(-\theta\omega) + \cos(-\theta\omega)^2 + \sin(-\theta\omega)^2 $$
1062+
# $$ = 1 - 2\cos(-\theta\omega) + 1 = 1 \rightarrow 1 - 2\cos(-\theta\omega) = 0 \rightarrow \cos(-\theta\omega) = \frac12 \rightarrow -\theta\omega =
1063+
# \pm\frac13\pi \approx \pm 1 \rightarrow \omega \approx \frac{1}{\theta} $$
1064+
1065+
# %%
1066+
w = np.logspace(-2, 2, 800)
1067+
fig = plt.figure()
1068+
plt.loglog(w, np.abs(1 - np.exp(-1 * w * 1j)))
1069+
plt.loglog(w, np.abs( np.exp(-1 * w * 1j)))
1070+
display(fig)
9871071

9881072
# %% [markdown]
9891073
# <div style="text-align:center;background-color:tomato;">End of lecture "Frequency Domain Design I & II"</div>
990-
1074+
# # Closing remark
1075+
# I'm so sorry, but everything we've done is, technically speaking, bachelor level control engineering.
3.37 KB
Binary file not shown.

helperFunctions.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,21 @@ def unwrap_angle(ang):
5353

5454
import matplotlib.pyplot as plt
5555
import numpy.random as rng
56+
import scipy as sp
57+
import control as cm
5658
from matplotlib.gridspec import GridSpec
5759
from matplotlib.ticker import MultipleLocator
58-
from IPython.display import display
60+
from IPython.display import display, Markdown
5961

6062
class loopShaper():
6163
def __init__(self, inputNoise=False, outputNoise=False):
6264
self.Czeros = []
6365
self.Cpoles = []
6466
self.Cgain = 1.
65-
# self.Pzeros = [1., self.gen_pair(1e-3, 1.5)]
66-
# self.Ppoles = [1e2, self.gen_pair(1e-1, .2), self.gen_pair(5e2, 3)]
67-
self.Pzeros = [self.gen_pair(-1e-2, 1.5)]
68-
self.Ppoles = [-1e2, self.gen_pair(-1e-1, .2)]
69-
self.Pgain = 1e3
70-
self.omRange = [-4, 4]
71-
self.OM = np.logspace(self.omRange[0], self.omRange[1], 500)
72-
self.S = self.OM*1j
67+
self.Pzeros = [-1., self.gen_pair(1e-3, 1.5)]
68+
self.Ppoles = [-1e2, self.gen_pair(1e-1, .2), self.gen_pair(5e2, 3)]
69+
self.Pgain = 1e6
70+
self.OM = np.logspace(-4, 4, 500)
7371

7472
def gen_pair(self, om0, zeta):
7573
q1 = -zeta*om0
@@ -79,29 +77,63 @@ def gen_pair(self, om0, zeta):
7977
def get_response(self, Z, P, K):
8078
response = np.ones_like(self.S) * K
8179
for z in Z:
80+
if z == []: continue
8281
if type(z) == tuple: # zero pair
8382
response *= (self.S - z[0]) * (self.S - z[1])
8483
else:
8584
response *= (self.S - z)
8685
for p in P:
86+
if p == []: continue
8787
if type(p) == tuple: # pole pair
8888
response /= (self.S - p[0])
8989
response /= (self.S - p[1])
9090
else:
9191
response /= (self.S - p)
9292
return response
9393

94+
def get_CLsteadygain(self):
95+
Lzeros, Lpoles = self.Pzeros + self.Czeros, self.Ppoles + self.Cpoles
96+
Lgain = self.Pgain * self.Cgain
97+
98+
NL, DL, = 1, 1
99+
for z in Lzeros:
100+
if z == []: continue
101+
if type(z) == tuple: # zero pair
102+
NL *= ( - z[0]) * ( - z[1])
103+
else:
104+
NL *= ( - z)
105+
for p in Lpoles:
106+
if p == []: continue
107+
if type(p) == tuple: # pole pair
108+
DL *= ( - p[0]) * ( - p[1])
109+
else:
110+
DL *= ( - p)
111+
112+
return np.abs(Lgain * NL / (DL + Lgain*NL))
113+
114+
def unpack(self, Q):
115+
out = []
116+
for q in Q:
117+
if q == []: continue
118+
if type(q) == tuple: # pair
119+
out.append(q[0])
120+
out.append(q[1])
121+
else:
122+
out.append(q)
123+
return out
124+
94125

95126
def plot_LS(self, ax):
96-
[a.set_title(b) for a, b in zip([ax[-1], ax[0]], ["Nyquist plot", "Bode plots"])]
97-
[a.set_ylabel(b) for a, b in zip(ax[:3], ["$|L(s)|$", r"$\angle L(s)$", "$|S(s)|$" ])]
127+
[a.set_title(b) for a, b in zip([ax[3], ax[0], ax[4]], ["Nyquist plot", "Bode plots", "Step Response"])]
128+
[a.set_ylabel(b) for a, b in zip([ax[0], ax[1], ax[2], ax[4]],
129+
["$|L(s)|$", r"$\angle L(s)$", "$|S(s)|$" , "$y(t)$" ])]
98130
ax[1].yaxis.set_major_locator(MultipleLocator(90))
99131
ax[2].set_xlabel(r"$\omega$")
100-
ax[-1].set(aspect='equal',
132+
ax[4].set_xlabel(r"$t$")
133+
ax[3].set(aspect='equal',
101134
xlabel=r"$\mathfrak{Re}\{L(\Gamma_s)\}$", ylabel=r"$\mathfrak{Im}\{L(\Gamma_s)\}$",
102135
xlim=[-2,1], ylim=[-2, 2])
103136

104-
self.OM = np.logspace(self.omRange[0], self.omRange[1], 500)
105137
self.S = self.OM*1j
106138

107139
Presponse = self.get_response(self.Pzeros, self.Ppoles, self.Pgain)
@@ -121,14 +153,33 @@ def plot_LS(self, ax):
121153
ax[1].legend(handles=[llM, lpM, lcM])
122154
[a.autoscale(enable=True, axis='x', tight=True) for a in ax[:3]]
123155

156+
124157
# Plot sensitivity
125158
ax[2].loglog(self.OM, np.abs(1. / (1. + Lresponse)), 'k')
126159

160+
[a.axhline(1, color='r', lw=.7) for a in [ax[0], ax[2]]]
161+
127162
# Plots Nyquist
128163
ax[3].plot(Lresponse.real, Lresponse.imag, 'k')
129164
ax[3].plot(Lresponse.real, -Lresponse.imag, 'k--')
130165

131-
UnitCirc = np.exp(np.linspace(0,2*np.pi,100))
166+
UnitCirc = np.exp(np.linspace(0,2*np.pi,100)*1j)
132167
ax[3].plot(UnitCirc.real, UnitCirc.imag, 'k:')
133168
ax[3].plot([-1], [0], 'rx')
134169

170+
# Plot step response
171+
P = cm.zpk(self.unpack(self.Pzeros), self.unpack(self.Ppoles), self.Pgain)
172+
C = cm.zpk(self.unpack(self.Czeros), self.unpack(self.Cpoles), self.Cgain)
173+
L = cm.series(C, P)
174+
T = cm.feedback(L)
175+
t, yout = cm.step_response(L)
176+
ax[4].plot(t, yout, 'k')
177+
ax[4].autoscale(enable=True, axis='x', tight=True)
178+
179+
# Plot steady response
180+
Tss = self.get_CLsteadygain()
181+
ax[4].axhline(Tss, color='k', ls=':')
182+
183+
print(f"Steady gain is {Tss:.3E}")
184+
display(Markdown(f"$M_S = {np.abs(1. / (1. + Lresponse)).max():.3f}$"))
185+

scratch.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
plt.rcParams.update({ 'mathtext.fontset': 'cm',
4+
'font.size': 12.0, 'axes.labelsize': 'medium',
5+
'xtick.labelsize': 'x-small', 'ytick.labelsize': 'x-small',
6+
'axes.grid': True, 'axes.formatter.limits': [-3, 6],
7+
'grid.alpha': 0.5, 'figure.figsize': [11.0, 4],
8+
'figure.constrained_layout.use': True, 'scatter.marker': 'x',
9+
'animation.html': 'jshtml'
10+
})
11+
12+
from matplotlib.ticker import MultipleLocator
13+
from matplotlib.gridspec import GridSpec
14+
15+
import warnings
16+
warnings.filterwarnings("ignore")
17+
18+
import control as cm
19+
from helperFunctions import *
20+
21+
###############################################
22+
SYS = loopShaper()
23+
fig = plt.figure(figsize=[15, 8])
24+
gs = GridSpec(4,2, figure=fig)
25+
ax = [fig.add_subplot(a) for a in [gs[0,0], gs[1, 0], gs[2, 0], gs[:3, 1], gs[3,:]]]
26+
27+
SYS.plot_LS(ax)
28+
plt.show()

0 commit comments

Comments
 (0)