77
88
99# %%
10+ % matplotlib notebook
1011import numpy as np
1112import matplotlib .pyplot as plt
12- # %matplotlib inline
1313plt .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
2423import warnings
2524warnings .filterwarnings ("ignore" )
6362#
6463
6564# %%
65+ ## Eigenvalues
6666sig1 = - 0.8
6767sig2 = 3 + 15j
6868sig3 = - 0.5 + 5j
6969
70- ###### Processing and Plotting #########
70+ ###### Plotting #########
7171fig , 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]
102101# ### Block diagrams
103102# *Pixel perfect ways to visualise systems*
104103#
105- # 
104+ # 
106105#
107106# It's a nice sanity check that any fundamental block scheme of an $n$-th order system has $n$ integrators.
108107#
196195h = 0.1
197196for 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
202201T = np .arange (max (traj .shape ))* h
203202l1 , = ax [1 ].plot (T , traj [0 ,:], label = "$x_1$" )
210209else :
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
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
249249P_res = cm .rss (20 )
250250P_res .D = [rng .rand ()]
251251
278278####### Plotting #######
279279fig , 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 )
286286l1 , = ax [2 ,0 ].plot (impResponse .time , impTrans , label = "Transient" )
287287l2 , = 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 )
294294l1 , = ax [2 ,1 ].plot (stpResponse .time , stpTrans , label = "Transient" )
295295l2 , = 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$$
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- # 
333+ # 
333334#
334335# Output feedback is stuff like PID, or in other terms: disgusting. We'll look at state feedback now! *The real stuff.*
335336
348349####### Plotting #######
349350fig , ax = plt .subplots ()
350351ax .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):
381383ax .plot (unforced_reg .time , unforced_reg .outputs , '--' , color = "tab:orange" , label = "Unforced" )
382384ax .plot (forced_reg .time , forced_reg .outputs , color = "tab:blue" , label = "Closed Loop" )
383385ax .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- # 
393+ # 
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):
417420ax [1 ].plot (response_saw_ref .time , response_saw_ref .inputs , '--' , color = "tab:orange" , label = "Reference" )
418421ax [1 ].plot (response_saw_ref .time , response_saw_ref .outputs , color = "tab:blue" , label = "CL System" )
419422ax [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- # 
436+ # 
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):
465469ax [1 ].plot (response_saw_ref .time , response_saw_ref .outputs , color = "tab:blue" , label = "CL System" )
466470ax [1 ].plot (response_saw_int .time , response_saw_int .outputs , ':' , color = "tab:green" , label = "System + integral" )
467471ax [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):
518523ax [1 ].plot (response_saw_ref_pert .time , response_saw_ref_pert .outputs , color = "tab:blue" , label = "CL System" )
519524ax [1 ].plot (response_saw_int_pert .time , response_saw_int_pert .outputs , ':' , color = "tab:green" , label = "System + integral" )
520525ax [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
558563display (Markdown (rf'$\lambda_1 = -\zeta\omega_0+\omega_0\sqrt{ "{" } \zeta^2-1{ "}" } = { - zeta * omega0 + omega0 * np .emath .sqrt (zeta ** 2 - 1 ):.3f} $' ))
559564display (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):
622629l0 = ax [1 ].plot (response_dom .time , response_dom .outputs , color = "tab:blue" , label = "Original sys." )
623630l1 = ax [1 ].twinx ().plot (response_dom_2d .time , response_dom_2d .outputs , '--' , color = "tab:orange" , label = "2nd order sys." )
624631ax [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
683691ax [1 ].plot (response_pp .time , (- K_pp @ response_pp .states )[0 ,:] + kf_pp * response_pp .inputs , color = "tab:blue" , label = "Pole Placement" )
684692ax [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):
724733nx = len (P_obs .poles ())
725734x0 = 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!
730739def 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):
772781L_slow = cm .place (P_obs .A .transpose (), P_obs .C .transpose (), obsv_poles_slow ).transpose ()
773782L_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+
775787P_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
816828e_slow = lin .eigvals (P_obs .A - L_slow @ P_obs .C )
817829e_fast = lin .eigvals (P_obs .A - L_fast @ P_obs .C )
830+ display (fig )
818831
819832fig , 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
0 commit comments