Skip to content

Commit 1a4542e

Browse files
committed
Sensitivity Analysis
1 parent d28c06c commit 1a4542e

13 files changed

Lines changed: 424 additions & 1592 deletions

SysReg_crashcourse_FreqDomain_BlockB.py

Lines changed: 272 additions & 55 deletions
Large diffs are not rendered by default.

SysReg_crashcourse_TimeDomain_BlockA.ipynb

Lines changed: 0 additions & 1483 deletions
This file was deleted.

SysReg_crashcourse_TimeDomain_BlockA.py

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@
77

88

99
# %%
10+
%matplotlib notebook
1011
import numpy as np
1112
import matplotlib.pyplot as plt
12-
# %matplotlib inline
1313
plt.rcParams.update({ 'text.usetex': False, 'mathtext.fontset': 'cm',
1414
'font.size': 12.0, 'axes.labelsize': 'medium',
1515
'xtick.labelsize': 'x-small', 'ytick.labelsize': 'x-small',
1616
'axes.grid': True, 'axes.formatter.limits': [-3, 6],
1717
'grid.alpha': 0.5, 'figure.figsize': [11.0, 4],
1818
'figure.constrained_layout.use': True, 'scatter.marker': 'x',
19-
'savefig.dpi': 300, 'savefig.bbox': 'tight',
20-
'savefig.pad_inches': 0.05, 'savefig.transparent': True})
19+
'animation.html': 'jshtml'})
2120

22-
from IPython.display import display, Markdown, HTML
21+
from IPython.display import display, Markdown
2322

2423
import warnings
2524
warnings.filterwarnings("ignore")
@@ -63,25 +62,25 @@
6362
#
6463

6564
# %%
65+
## Eigenvalues
6666
sig1 = -0.8
6767
sig2 = 3 + 15j
6868
sig3 = -0.5 + 5j
6969

70-
###### Processing and Plotting #########
70+
###### Plotting #########
7171
fig, ax = plt.subplots(1,3)
72-
idx = 0
73-
for sig in [sig1, sig2, sig3]:
74-
t = np.linspace(0, 3/abs(np.real(sig)), num=300)
75-
if np.iscomplex(sig):
76-
l1, = ax[idx].plot(t, np.exp(np.real(sig)*t), 'r--', label=f"$e^{r"{"}{sig.real}t{r"}"}$")
77-
ax[idx].plot(t, -np.exp(np.real(sig)*t), 'r--')
78-
l2, = ax[idx].plot(t, 2*np.cos(np.imag(sig)*t), 'k', alpha=0.3, label=f"$2\cos({sig.imag} t)$")
72+
for sig, idx in zip([sig1, sig2, sig3], range(3)):
73+
t = np.linspace(0, 3/abs(np.real(sig)), num=300) # Adapt to convergence speed
74+
if np.iscomplex(sig): # Plot decomposition
75+
l1, = ax[idx].plot(t, np.exp(sig.real * t), 'r--', label=f"$e^{r"{"}{sig.real}t{r"}"}$") # Upper envelope
76+
ax[idx].plot(t, -np.exp(sig.real * t), 'r--') # Lower envelope
77+
l2, = ax[idx].plot(t, 2*np.cos(sig.imag * t), 'k', alpha=0.2, label=f"$2\cos({sig.imag} t)$") # Oscillation
7978
ax[idx].legend(handles=[l1, l2])
80-
ax[idx].plot(t, np.exp(sig*t))
79+
ax[idx].plot(t, np.exp(sig * t), 'k') # Trajectory
8180
ax[idx].set(title=f"$\lambda = {sig}$", xlabel="$t$")
82-
idx += 1
8381

84-
_ = ax[0].set_ylabel("$y(t)$")
82+
ax[0].set_ylabel("$y(t)$")
83+
display(fig)
8584

8685

8786
# %% [markdown]
@@ -102,7 +101,7 @@
102101
# ### Block diagrams
103102
#         *Pixel perfect ways to visualise systems*
104103
#
105-
# ![Slide about block diagram elements](BlockDiagEls.png)
104+
# ![Slide about block diagram elements](figures/BlockDiagEls.png)
106105
#
107106
# It's a nice sanity check that any fundamental block scheme of an $n$-th order system has $n$ integrators.
108107
#
@@ -196,8 +195,8 @@
196195
h = 0.1
197196
for idx in range(max(traj.shape)-1):
198197
traj[:, idx+1] = RK4(x=traj[:, idx], dx=lambda x: A@x, h=h)
199-
_ = ax[0].plot(traj[0,:], traj[1,:])
200-
_ = ax[0].scatter(traj[0,0], traj[1,0], marker='o', color='tab:blue', facecolors='none')
198+
ax[0].plot(traj[0,:], traj[1,:])
199+
ax[0].scatter(traj[0,0], traj[1,0], marker='o', color='tab:blue', facecolors='none')
201200

202201
T = np.arange(max(traj.shape))*h
203202
l1, = ax[1].plot(T, traj[0,:], label="$x_1$")
@@ -210,8 +209,8 @@
210209
else:
211210
ax[1].legend(handles=[l1,l2])
212211

213-
_ = ax[1].set(xlim=[0, max(T)], title="State trajectory", xlabel="$t$", ylabel="$x$")
214-
212+
ax[1].set(xlim=[0, max(T)], title="State trajectory", xlabel="$t$", ylabel="$x$")
213+
display(fig)
215214

216215
# %% [markdown]
217216
# ### How to be an eigenvalue wizard
@@ -246,6 +245,7 @@
246245
# The impulse signal is primarily used to see the transient behaviour of the system and the step functions shows the transient and steady state behaviour. Look at these plots to see! We're even able to seperate the transient from the steady state for the step!
247246

248247
# %%
248+
# Create random system
249249
P_res = cm.rss(20)
250250
P_res.D = [rng.rand()]
251251

@@ -278,22 +278,23 @@
278278
####### Plotting #######
279279
fig, ax = plt.subplots(3,2, sharex='col', figsize=[11, 7])
280280

281-
_ = ax[0,0].set(title="Impulse response", ylabel="$u$")
282-
_ = ax[1,0].set(ylabel="$y$")
283-
_ = ax[2,0].set(xlabel="$t/s$", ylabel="$y$ - decomposed")
284-
_ = ax[0,0].plot(impResponse.time, impResponse.inputs)
285-
_ = ax[1,0].plot(impResponse.time, impResponse.outputs)
281+
ax[0,0].set(title="Impulse response", ylabel="$u$")
282+
ax[1,0].set(ylabel="$y$")
283+
ax[2,0].set(xlabel="$t/s$", ylabel="$y$ - decomposed")
284+
ax[0,0].plot(impResponse.time, impResponse.inputs)
285+
ax[1,0].plot(impResponse.time, impResponse.outputs)
286286
l1, = ax[2,0].plot(impResponse.time, impTrans, label="Transient")
287287
l2, = ax[2,0].plot(impResponse.time, np.squeeze(P_res.D*impResponse.inputs), '--', label="Feedthrough")
288-
_ = ax[2,0].legend(handles=[l1,l2])
288+
ax[2,0].legend(handles=[l1,l2])
289289

290-
_ = ax[0,1].set(title="Step response")
291-
_ = ax[2,1].set(xlabel="$t / s$")
292-
_ = ax[0,1].plot(stpResponse.time, stpResponse.inputs)
293-
_ = ax[1,1].plot(stpResponse.time, stpResponse.outputs)
290+
ax[0,1].set(title="Step response")
291+
ax[2,1].set(xlabel="$t / s$")
292+
ax[0,1].plot(stpResponse.time, stpResponse.inputs)
293+
ax[1,1].plot(stpResponse.time, stpResponse.outputs)
294294
l1, = ax[2,1].plot(stpResponse.time, stpTrans, label="Transient")
295295
l2, = ax[2,1].plot(stpResponse.time, stpStead, '--', label="Steady State")
296-
_ = ax[2,1].legend(handles=[l1,l2])
296+
ax[2,1].legend(handles=[l1,l2])
297+
display(fig)
297298

298299
# %% [markdown]
299300
# Decomposing a signal into a transient and steady state part? How is that possible? It really comes down to seperating the time dependent and time independent parts. By convolution of the input, we can express the state and output trajectory of any LTI system as $$ x(t) = e^{At}x_0 + \int_0^t e^{A(t-\tau)}Bu(\tau)d\tau$$
@@ -329,7 +330,7 @@
329330
# ## Feedback control
330331
# WOW ARE WE GOING TO DO CONTROL NOW??? *FINALLY*???? Yes! No... There's one more difference we need to explain. There are two types of feedback control, output and state feedback. The difference is in the name and here you also see the difference in a block diagram for a plant/process/system, $P$, and a controller, $C$.
331332
#
332-
# ![output vs state feedback](xyFB.svg)
333+
# ![output vs state feedback](figures/xyFB.svg)
333334
#
334335
# Output feedback is stuff like PID, or in other terms: disgusting. We'll look at state feedback now! *The real stuff.*
335336

@@ -348,7 +349,8 @@
348349
####### Plotting #######
349350
fig, ax = plt.subplots()
350351
ax.plot(unforced_reg.time, unforced_reg.outputs)
351-
_ = ax.set(title="Unforced open loop response of $P$, non-zero initialisation", xlabel="$t/s$", ylabel="$y$")
352+
ax.set(title="Unforced open loop response of $P$, non-zero initialisation", xlabel="$t/s$", ylabel="$y$")
353+
display(fig)
352354

353355
# %% [markdown]
354356
#
@@ -381,13 +383,14 @@ def isControlable(A, B):
381383
ax.plot(unforced_reg.time, unforced_reg.outputs, '--', color="tab:orange", label="Unforced")
382384
ax.plot(forced_reg.time, forced_reg.outputs, color="tab:blue", label="Closed Loop")
383385
ax.legend()
384-
_ = ax.set(title="Closed loop response of $P$ and $K$, non-zero initialisation", xlabel="$t/s$", ylabel="$y$")
386+
ax.set(title="Closed loop response of $P$ and $K$, non-zero initialisation", xlabel="$t/s$", ylabel="$y$")
387+
display(fig)
385388

386389
# %% [markdown]
387390
#
388391
# However, regulating to zero is boring, I want to be able to tell my system to do a backflip! We need to add a reference, and I'll spoil that we also need a reference gain $k_f$:
389392
#
390-
# ![Reference tracking](refTrackCL.svg)
393+
# ![Reference tracking](figures/refTrackCL.svg)
391394
#
392395
# So how does this work then... Same substitution as before!
393396
# $$ \dot x = Ax + Bu = Ax + B(k_f r - Kx) = (A-BK) x + Bk_f r.$$
@@ -417,8 +420,9 @@ def isControlable(A, B):
417420
ax[1].plot(response_saw_ref.time, response_saw_ref.inputs , '--', color="tab:orange", label="Reference")
418421
ax[1].plot(response_saw_ref.time, response_saw_ref.outputs, color="tab:blue", label="CL System")
419422
ax[0].legend()
420-
_ = ax[0].set(title="Closed loop reference tracking", ylabel="$y$")
421-
_ = ax[1].set(xlabel="$t/s$", ylabel="$y$")
423+
ax[0].set(title="Closed loop reference tracking", ylabel="$y$")
424+
ax[1].set(xlabel="$t/s$", ylabel="$y$")
425+
display(fig)
422426

423427
# %% [markdown]
424428
#
@@ -429,7 +433,7 @@ def isControlable(A, B):
429433
#
430434
# So if out model is incorrect, what does that mean for our controller? Simply said, it's not exactly what we want, but *it's close*. To get it perfect however, we need to add an output feedback part to our state feedback controller. An integrator of the reference-output-error to be precise. This integrator wil drive the integral of the error, $z$, to zero, meaning that the error will be zero. I'll stop talking now and show the block diagram so you actually understand.
431435
#
432-
# ![Integral action](IntActCL.svg)
436+
# ![Integral action](figures/IntActCL.svg)
433437
#
434438
# Sooooo maths time. Substitute everything into everything, yada yada, this is largely what control engineers do. Also assume D=0. Lets start with plant equations and work our way backwards through the block diagram.
435439
#
@@ -465,8 +469,9 @@ def isControlable(A, B):
465469
ax[1].plot(response_saw_ref.time, response_saw_ref.outputs, color="tab:blue", label="CL System")
466470
ax[1].plot(response_saw_int.time, response_saw_int.outputs, ':', color="tab:green", label="System + integral")
467471
ax[0].legend()
468-
_ = ax[0].set(title="Closed loop reference tracking with integral action", ylabel="$y$")
469-
_ = ax[1].set(xlabel="$t/s$", ylabel="$y$")
472+
ax[0].set(title="Closed loop reference tracking with integral action", ylabel="$y$")
473+
ax[1].set(xlabel="$t/s$", ylabel="$y$")
474+
display(fig)
470475

471476
# %% [markdown]
472477
# And now we face dreadful reality and perturb our plant a little!
@@ -518,9 +523,9 @@ def isControlable(A, B):
518523
ax[1].plot(response_saw_ref_pert.time, response_saw_ref_pert.outputs, color="tab:blue", label="CL System")
519524
ax[1].plot(response_saw_int_pert.time, response_saw_int_pert.outputs, ':', color="tab:green", label="System + integral")
520525
ax[0].legend()
521-
_ = ax[0].set(title="Closed loop reference tracking with perturbed plant", ylabel="$y$")
522-
_ = ax[1].set(xlabel="$t/s$", ylabel="$y$")
523-
526+
ax[0].set(title="Closed loop reference tracking with perturbed plant", ylabel="$y$")
527+
ax[1].set(xlabel="$t/s$", ylabel="$y$")
528+
display(fig)
524529

525530
# %% [markdown]
526531
# See how bad the system without the integral action performs? (If not, randomise the plant again...)
@@ -557,6 +562,7 @@ def isControlable(A, B):
557562

558563
display(Markdown(rf'$\lambda_1 = -\zeta\omega_0+\omega_0\sqrt{"{"}\zeta^2-1{"}"} = {-zeta*omega0 + omega0*np.emath.sqrt(zeta**2-1):.3f}$'))
559564
display(Markdown(rf'$|\lambda_1| = \omega_0 = {np.abs(P_2d.poles()[0]):.3f}$'))
565+
display(fig)
560566

561567
# %% [markdown]
562568
# Now lets look at the step responses! We have three (or four) terms for intervals of $\zeta$:
@@ -584,7 +590,8 @@ def isControlable(A, B):
584590

585591
[ax[0,p].set(title=f"$\zeta={Zeta[p]}$") for p in range(len(Zeta))]
586592
[ax[p,0].set(ylabel=f"$\omega_0={Omega0[p]}$") for p in range(len(Omega0))]
587-
_ = [ax[2,p].set(xlabel=f"$t/s$") for p in range(len(Zeta))]
593+
[ax[2,p].set(xlabel=f"$t/s$") for p in range(len(Zeta))]
594+
display(fig)
588595

589596
# %% [markdown]
590597
# ### Why are second order systems so important?
@@ -622,7 +629,8 @@ def isControlable(A, B):
622629
l0 = ax[1].plot(response_dom.time, response_dom.outputs, color="tab:blue", label="Original sys.")
623630
l1 = ax[1].twinx().plot(response_dom_2d.time, response_dom_2d.outputs, '--', color="tab:orange", label="2nd order sys.")
624631
ax[1].legend(handles = [l0[0], l1[0]])
625-
_ = ax[1].set(title="Step response (ignore scaling)", xlabel="t/s")#, yticks=[])
632+
ax[1].set(title="Step response (ignore scaling)", xlabel="t/s")#, yticks=[])
633+
display(fig)
626634

627635
# %% [markdown]
628636
# ## Art is for suckers and should be optimised
@@ -682,7 +690,8 @@ def isControlable(A, B):
682690

683691
ax[1].plot(response_pp.time , (-K_pp @ response_pp.states)[0,:] + kf_pp*response_pp.inputs, color="tab:blue", label="Pole Placement")
684692
ax[1].plot(response_lqr.time, (-K_lqr @ response_lqr.states)[0,:] + kf_lqr*response_lqr.inputs, color="tab:brown", label="LQR")
685-
_=ax[1].set(ylabel="$u$", xlabel="$t$ / s")
693+
ax[1].set(ylabel="$u$", xlabel="$t$ / s")
694+
display(fig)
686695

687696
# %% [markdown]
688697
# <div style="text-align:center;background-color:tomato;">End of lecture 7</div>
@@ -724,7 +733,7 @@ def isControlable(A, B):
724733
nx = len(P_obs.poles())
725734
x0 = rng.randn(nx,1)
726735

727-
T_obs = np.linspace(0., 20., 400)
736+
T_obs = np.linspace(0., 5./abs(P_obs.poles().real.max()), 400)
728737

729738
## Check observability!
730739
def isObservable(A, C):
@@ -759,8 +768,8 @@ def isObservable(A, C):
759768
ax[nx-1].plot(response_obs.time, response_obs.states[nx-1, :], 'k', alpha=.3, label=r"$x$")[0],
760769
ax[nx-1].plot(response_obs.time, response_obs.states[nx-1+nx, :], 'k--', label=r"$\hat x$")[0]
761770
])
762-
_ = ax[nx-1].set(ylabel = f"$x_{nx-1}$", xlabel = "$t$ / s")
763-
771+
ax[nx-1].set(ylabel = f"$x_{nx-1}$", xlabel = "$t$ / s")
772+
display(fig)
764773

765774
# %% [markdown]
766775
# ### Quicker than thou
@@ -772,6 +781,9 @@ def isObservable(A, C):
772781
L_slow = cm.place(P_obs.A.transpose(), P_obs.C.transpose(), obsv_poles_slow).transpose()
773782
L_fast = cm.place(P_obs.A.transpose(), P_obs.C.transpose(), obsv_poles_fast).transpose()
774783

784+
T_obs = np.linspace(0., 10./abs(obsv_poles_fast.real.max()), 400)
785+
obsIn = np.sin(T_obs)**1.7
786+
775787
P_obs_aug_slow = cm.ss(np.block([[P_obs.A, np.zeros_like(P_obs.A)], # A
776788
[L_slow@P_obs.C, P_obs.A - L_slow@P_obs.C]]),
777789
np.vstack((P_obs.B, P_obs.B)), # B
@@ -815,13 +827,15 @@ def isObservable(A, C):
815827

816828
e_slow = lin.eigvals(P_obs.A - L_slow @ P_obs.C)
817829
e_fast = lin.eigvals(P_obs.A - L_fast @ P_obs.C)
830+
display(fig)
818831

819832
fig, ax = plt.subplots()
820-
_ = ax.legend(handles= [
833+
ax.legend(handles= [
821834
ax.scatter(P_obs.poles().real, P_obs.poles().imag, s=100, marker='x', color='tab:blue', label="System poles"),
822835
ax.scatter(e_slow.real, e_slow.imag, s=100, marker='+', color='tab:orange', label="Slow error poles"),
823836
ax.scatter(e_fast.real, e_fast.imag, s=100, marker='1', color='tab:green', label="Fast error poles"),
824837
])
838+
display(fig)
825839

826840
# %% [markdown]
827841
# ## Connecting the dots
@@ -960,6 +974,8 @@ def isObservable(A, C):
960974
ax[idx].plot(response.time, response.outputs, 'k')
961975
ax[idx].set(ylabel="$y$", title=title)
962976

977+
display(fig)
978+
963979
# %% [markdown]
964980
# Run the previous cell a few times... fun fact: there is no generic fix for control problems...
965981

6.1 KB
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)