#!/usr/bin/env python
# coding: utf-8
#
#
#
# # Stabilising an Inverted Pendulum on a Cart
#
# ### Examples - Mechanics
#
# By Eilif Sommer Øyre, Niels Henrik Aase and Jon Andreas Støvneng
#
# Last edited: April 20th 2019
#
# ___
# ## Introduction
# The inverted pendulum is a classic controllability example. Most likely, you have performed this kind of control yourself by balancing a broom or stick on your palm. In that case the pendulum was a physical pendulum and your arm the external driving force. In this notebook we will try to balance an inverted mathematical pendulum on a cart subjected by an external force acting on the cart's center of mass.
#
#
#
#
# **Figure 1:** MIT Professor Alan V. Oppenheim in his lecture *The Inverted Pendulum* from the course *Signals and Systems*. Source: Oppenheim's lecture notes [[1]](#rsc).
#
# The figure above shows a common experimental setup for controlling an inverted pendulum. The pendulum is moved by pulling a suspended wire between the cart and an electrical motor. In this particular setup, the controller will balance the pendulum independently of the mass of the pendulum "bead", explaining the confidence when pouring more liquid into the glass.
#
# Initially, we will model our pendulum system using Lagrangian mechanics and later control the pendulum using proportional control and then construct an algorithm to optimise the regulation based on quadratic weighing factors (linear quadratic regulator [LQR])
# In[6]:
import numpy as np
import progressbar
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import display, Image
from scipy import signal
import control
get_ipython().run_line_magic('matplotlib', 'inline')
# ## Theory Part I
# Figure 2 below shows a schematic of the system used in this example. The inverted pendulum is fixed in one end on the top of a cart with a mass $M$. The rotation around the fixed point is assumed frictionless. The bead in the end of the pendulum has a mass $m$, while the stiff rod connecting the bead to the cart is massless. This makes the inverted pendulum a mathematical pendulum. The cart is able to move relative to the surface, i.e. only in the $x$-direction. Wheels are only included for illustration. The forces acting on the bead is the conservative gravitational force $\vec{G}$ in the negative $y$-direction, while the force acting on the cart is the external applied force $\vec{u}$ in the $x$-direction. The force $\vec{F}$ is an approximation of the friction on the entire system, also acting in the $x$-direction. The length of the massless rod is assigned $l$, and the angular displacement of the pendulum relative to $\vec{y}$ is given by $\theta$.
#
#
#
#
#
# **Figure 2:** *A schematic on the inverted pendulum. The bead angle $\theta$ is zero at the unstable (upright) equilibrium. The gravitational force, $\vec{G}$, acting on the bead with mass $m$, the friction on the system $\vec{F}$ and external force $\vec{u}$ is illustrated.*
# In[7]:
figSizeX = 12
figSizeY = 9
# System parameters
m = 0.1 # mass of bead [kg]
M = 1 # mass of cart [kg]
l = 0.2 # length of rod [m]
g = 9.81 # gravitational acceleration [m s^-2]
mu = 10 # friction coefficient [kg s^-1]
# Initial conditions of generalised coordinates
x0_0 = 0 # cart position [m]
x1_0 = 0 # rod angle [radians]
x2_0 = 0 # cart velocity [m s^-1]
x3_0 = 0 # rod angular velocity [radians s^-1]
u_0 = 0 # initial cart force [kg m s^-2]
# The system parameters above can be altered to get different motion and response of the system. As example, a more massive bead would require more external force to be balanced, and a longer rod would reduce the pendulum swing period.
# ### Equations of motion
# It is possible to derive the equations of motion using Newtonian forces, but the Lagrangian formulation is more elegant. A full derivation of the equations from the Lagrangian
# $$ L = T - U $$
# is found in [appendix A](#apndxA).
#
# The system sketched in [figure 2](#img2) has two holonomic constraints: The rod length $l$ is fixed, and the $y$-coordinate of the cart is always zero. This results in two generalised coordinates: The bead angle $\theta$, the cart position $x$ and their time derivatives $\dot{\theta}$ and $\dot{x}$. The corresponding equations of motion evaluates to
#
# \begin{equation}
# \ddot{x} = \lambda(\theta)\big[mgl^2\dot{\theta}^2\sin\theta - mgl \sin\theta \cos\theta - l\mu \dot{x} + ul \big]
# \label{cartacc}
# \end{equation}
#
# \begin{equation}
# \ddot{\theta} = \lambda(\theta)\big[(m + M)g \sin\theta - mgl\dot{\theta}^2 \sin\theta \cos\theta + \mu\dot{x}\cos\theta - u\cos\theta \big]
# \label{angelacc}
# \end{equation}
#
# where
#
# \begin{equation}
# \lambda(\theta) = \frac{1}{l(M + m\sin^2\theta)}
# \label{lambda} \text{.}
# \end{equation}
#
# The friction coefficient $\mu$ originates from the friction force $\vec{F}$, which is set to be proportional to the cart velocity, $F = -\mu\dot{x}$.
#
# By renaming the generalized coordinates we get the *state vector*:
#
# \begin{equation}
# \textbf{x} =
# \begin{bmatrix} x_0 \\
# x_1 \\
# x_2 \\
# x_3
# \end{bmatrix} =
# \begin{bmatrix} x \\
# \theta \\
# \dot{x} \\
# \dot{\theta}
# \end{bmatrix} \text{.}
# \end{equation}
#
# Then, by realising that $\dot{x_0} = x_2$ and $\dot{x_1} = x_3$, we get the *state equation*:
#
# \begin{equation}
# \dot{\textbf{x}} = \frac{d}{dt}
# \begin{bmatrix} x_0 \\
# x_1 \\
# x_2 \\
# x_3
# \end{bmatrix} =
# \begin{bmatrix} x_2 \\
# x_3 \\
# \lambda(x_1)\big[ml^2{x_3}^2\sin x_1 - mgl \sin x_1 \cos x_1 - l\mu x_2 + ul \big] \\
# \lambda(x_1)\big[(m + M)g \sin x_1 - ml{x_3}^2 \sin x_1 \cos x_1 + \mu x_2\cos x_1 - u\cos x_1 \big]
# \end{bmatrix} \text{.}
# \label{stateEquation}
# \end{equation}
#
# The last two equations in the state vector are non-linear. Which means that the motion of the system is very hard to regulate. Consequently, we will have to linearise them. This is done in part II of the notebook.
# ## Implementation - Part I
# ### Simulating the system
#
# To simulate the inverted pendulum on a cart we have to solve the state equation iteratively. In this notebook we will use the numerical method *Runge-Kutta fourth order* (RK4) to integrate the system of non-linear differential equations, \eqref{systemEquation}. If you have never heard of this method, or want a recap, check out [this notebook](https://nbviewer.jupyter.org/urls/www.numfys.net/media/notebooks/runge_kutta_method.ipynb) for a practical introduction.
#
# In addition to defining the RK4 step function, we define a function that returns the right hand side of \eqref{stateEquation} given a vector $\textbf{x}$ and the system parameters, and a function to calculate the systems potential and kinetic energy.
# In[20]:
def RHS(z, RHSargs):
"""Given a specified state vector and system parameters,
z and RHSargs respectively, this function returns the
state vector element's time derivative, i.e. the right
hand side of the state equations.
Parameters:
z 4x1 array
RHSargs 6x1 array
Returns:
dxdt 4x1 array
"""
m, M, l, g, mu, u = RHSargs
x0, x1, x2, x3 = z
dx0dt = x2
dx1dt = x3
lambdax1 = 1/(l*(M + m*np.sin(x1)**2))
dx2dt = lambdax1*(m*l*l*x3**2*np.sin(x1) - m*g*l*np.sin(x1)*np.cos(x1)
- l*mu*x2 + u*l)
dx3dt = lambdax1*((m + M)*g*np.sin(x1) - m*l*x3*x3*np.sin(x1)*np.cos(x1)
+ mu*x2*np.cos(x1) - u*np.cos(x1))
dxdt = np.array([dx0dt, dx1dt, dx2dt, dx3dt])
return dxdt
def RK4Step(z, dt, RHS, RHSargs):
""" Performs one step of the 4th order Runge-Kutta method
z is the current state vector, dt is the numerical time
step, RHS is a function for the right hand side of the
system of differential equations, and RHSargs is the input
parameters for that function. RK4step returns the next
state vector, i.e. the values of z after a time dt.
Parameters:
z 4x1 array
dt float
RHS function
RHSargs 6x1 array
Returns:
The state vactor, z, after a time, dt.
"""
k1 = np.array(RHS(z, RHSargs))
k2 = np.array(RHS(z + k1*dt/2, RHSargs))
k3 = np.array(RHS(z + k2*dt/2, RHSargs))
k4 = np.array(RHS(z + k3*dt, RHSargs))
return z + dt/6*(k1 + 2*k2 + 2*k3 + k4)
# In[21]:
def addEnergy(z):
""" Given the the state vector, z, calculates and returns the kinetic
and potential energy of the system in an arrray. See appendix B for the
energy term formulas.
"""
x0, x1, x2, x3 = z
U = m*g*l*np.cos(x1)
T = 0.5*x2**2*(m + M) + 0.5*m*x3**2*l**2 + m*x2*x3*l*np.cos(x1)
return np.array([T, U])
# Now, we can integrate the state equation after defining a step size, $\Delta t$, and the integration duration $t_{max}$. The matrix ``Z`` is initialised to contain the state vector at any time $t$. While the initial angle of the pendulum is zero, it will never fall down because the x-component to the gravitational force is excactly zero. So, to make the situation more realistic, a random noise is added as external force, $u$, in every iteration. The noise could correspond to e.g. a wind gust (isolated on the cart).
# In[26]:
duration = 10 # Integration duration [sec]
timeStep = 1e-2 # Time step
n = int(duration/timeStep) # Number of datapoints
# Initialising matrices
RHSargs = np.array([m, M, l, g, mu, u_0]) # System parameters
Z = np.zeros((4, n)) # Z = [[x0_0, ... x0_n], [x1_0, ... x1_n], [x2,...,], [x3,...,]
# Inserting initial state vector into Z
Z[:, 0] = np.array([x0_0, x1_0, x2_0, x3_0])
# Initialising matrix containing the noise and potential force on the cart
extForce = np.zeros((2, n))
extForce[0, 0] = u_0
# Initialising matrix containing the kinetic and potential energy of the system
energy = np.zeros((2, n)) # energy = [[T_0 ... T_n], [U_0, ... U_n]]
energy[:, 0] = addEnergy([x0_0, x1_0, x2_0, x3_0])
bar = progressbar.ProgressBar()
for i in bar(range(n - 1)):
# Compute and insert the next state vector using RK4
Z[:, i + 1] = RK4Step(Z[:, i], timeStep, RHS, RHSargs)
# Generating a random noise value
noise = np.random.uniform(-0.001, 0.001)
extForce[1, i +1] = noise
# Updating the system parameter u
RHSargs[5] = noise
# Add the energy terms
energy[:, i + 1] = addEnergy(Z[:, i + 1])
# Next, we make a phase-space plot of the generalised coordinates in addition to the evolution to the state vector. The definition of the plotting function is found in [Appendix B](#apndxB).
# In[27]:
plotData(Z, extForce, energy, appliedForce=False)
# The phase-space plot of $\theta$ reveals a movement similar to a simple pendulum with friction. It spirals in towards zero angular velocity and an angle $\theta = \pi$. This point would correspond to the pendulum hanging downward at rest, which is the intuitive result if no force tries to maintain the inverted form. The two lowermost plots shows how the total energy of the system reduces with time. This is due to the friction force ($\vec{F}$ in [figure 2](#img2)) working in the opposite direction of the cart's velocity. The bigger the friction coefficient $\mu$, the higher the energy loss. As seen from the time evolution plots, the pendulum stays inverted some time before noise pushes it over. The phase-space path for $q_2(x, \dot{x})$ however has a not so harmonic motion. It seems to experience a high acceleration for a short time two times each periodic motion. Let's make an animation to see better whats going on. The definition of the animation function is found in [Appendix C](#apndxC). The animation below was generated using the code:
#
# generateAnimationWithPhaseSpace(Z, 'falling_phsp.gif')
#
# ![falling](https://www.numfys.net/media/notebooks/images/falling_phsp-kopi2.gif)
# As seen from the animation, the cart slides back and forth due to the pendulum.
#
# Now, in our state equation \eqref{stateEquation} (equations of motion) we have the variable $u$ which let us exert a external force on the cart. In the next part of the notebook we will utilise this variable to try to stabilise the pendulum. But first, the state equation has to be linearised.
# ## Theory Part II
# ### Linearising around equilibrium
# The non-linear equations of motion can be linearised to the form
#
# \begin{equation}
# \dot{\textbf{x}}
# = \mathcal{A}\textbf{x} + \mathcal{B}u
# \end{equation}
#
# around an equilibrium. The inverted pendulum in our example has two equilibrium points. The stable equilibrium, corresponding to a downward hanging pendulum
#
# \begin{equation}
# \begin{cases} x_0 &= \mathrm{free} \\
# x_1 &= \pi \\
# x_2 &= 0 \\
# x_3 &= 0
# \end{cases} .
# \end{equation}
#
# And the unstable equilibrium, corresponding to the pendulum being carefully balanced upright. It's unstable in the sense that any perturbation from its point, for example noise in the form of a small wind gust, will cause it to fall down.
#
# \begin{equation}
# \begin{cases} x_0 &= \mathrm{free} \\
# x_1 &= 0 \\
# x_2 &= 0 \\
# x_3 &= 0
# \end{cases} .
# \end{equation}
#
# The latter is the equilibrium we are interested in.
#
# The matrices $\mathcal{A}$ and $\mathcal{B}$ is given by the Jacobian Matrix,
#
# \begin{equation}
# \mathcal{J} = \big [ \frac{\partial \textbf{f}}{\partial x_1} \cdots \frac{\partial \textbf{f}}{\partial x_n} \big ] =
# \begin{bmatrix} \frac{\partial f_0}{\partial x_0} & \cdots & \frac{\partial f_0}{\partial x_n} \\
# \vdots & \ddots & \vdots \\
# \frac{\partial f_n}{\partial x_0} & \cdots & \frac{\partial f_n}{\partial x_n}
# \end{bmatrix}
# \end{equation}
#
# differentiated with respect to the variables $x$ and $u$, respectively [[2]](#rsc). The derivatives are then evaluated at an equilibrium point. Here $f_i$ is the components of the state equation $\eqref{stateEquation}$
#
# \begin{equation}
# \dot{\textbf{x}} = \frac{d}{dt}
# \begin{bmatrix} x_0 \\
# x_1 \\
# x_2 \\
# x_3
# \end{bmatrix} =
# \begin{bmatrix} f_0 \\
# f_1 \\
# f_2 \\
# f_3
# \end{bmatrix} =
# \begin{bmatrix} x_2 \\
# x_3 \\
# \lambda(x_1)\big[ml^2{x_3}^2\sin x_1 - mgl \sin x_1 \cos x_1 - l\mu x_2 + ul \big] \\
# \lambda(x_1)\big[(m + M)g \sin x_1 - ml{x_3}^2 \sin x_1 \cos x_1 + \mu x_2\cos x_1 - u\cos x_1 \big]
# \end{bmatrix} \text{.}
# \end{equation}
#
# When calculating the Jacobian matrix at the unstable equilibrium, every coordinate except $x_0$ goes to zero after differentiating. This gives
#
# \begin{equation}
# \mathcal{A} =
# \begin{bmatrix} \frac{\partial f_0}{\partial x_0} \big |_{0} & \frac{\partial f_0}{\partial x_1} \big |_{0} & \frac{\partial f_0}{\partial x_2} \big |_{0} & \frac{\partial f_0}{\partial x_3} \big |_{0} \\
# \frac{\partial f_1}{\partial x_0} \big |_{0} & \frac{\partial f_1}{\partial x_1} \big |_{0} & \frac{\partial f_1}{\partial x_2} \big |_{0} & \frac{\partial f_1}{\partial x_3} \big |_{0} \\
# \frac{\partial f_2}{\partial x_0} \big |_{0} & \frac{\partial f_2}{\partial x_1} \big |_{0} & \frac{\partial f_2}{\partial x_2} \big |_{0} & \frac{\partial f_3}{\partial x_3} \big |_{0} \\
# \frac{\partial f_3}{\partial x_0} \big |_{0} & \frac{\partial f_3}{\partial x_1} \big |_{0} & \frac{\partial f_3}{\partial x_2} \big |_{0} & \frac{\partial f_3}{\partial x_3} \big |_{0}
# \end{bmatrix} =
# \begin{bmatrix} 0 & 0 & 1 & 0 \\
# 0 & 0 & 0 & 1 \\
# 0 & \frac{-mg}{M} & \frac{-\mu}{M} & 0 \\
# 0 & \frac{g(m + M)}{lM} & \frac{\mu}{ML} & 0
# \end{bmatrix}
# \end{equation}
#
# and
#
# \begin{equation}
# \mathcal{B} =
# \begin{bmatrix} \frac{\partial f_0}{\partial u} \big |_{0} \\
# \frac{\partial f_1}{\partial u} \big |_{0} \\
# \frac{\partial f_2}{\partial u} \big |_{0} \\
# \frac{\partial f_3}{\partial u} \big |_{0}
# \end{bmatrix} =
# \begin{bmatrix} 0 \\
# 0 \\
# \frac{1}{M} \\
# \frac{-1}{Ml}
# \end{bmatrix} \text{.}
# \end{equation}
#
# One could perform a test to show that the system is controllable with the matrices $\mathcal{A}$ and $\mathcal{B}$, but in our case we will assume it is.
#
# Now, by defining the controller
#
# $$ u = -\mathcal{K}\textbf{x}, $$
#
# we get the linearised form
#
# \begin{equation}
# \dot{\textbf{x}} = (\mathcal{A} - \mathcal{BK})\textbf{x},
# \label{controller}
# \end{equation}
#
# where $\mathcal{K}$ is the gain matrix. It is a *proportional* gain as the control variable (the external force $u$) is *proportional* to the state vector $\text{bf}$. This allows us to choose a $\mathcal{K}$ such that the eigenvalues of the matrix in the parenthesis are all stable, in essence negative (which indicate negative feedback) [[1, 3]](#rsc).
# ## Implementation Part II
# ### Proportional gian controller
# In this part we do a similar implementation as above, in addition to calculate the gain matrix. Then, by adding the control force in each integration step, a feedback loop is created. To retrieve a $\mathcal{K}$ matrix such that the eigenvalues of $(\mathcal{A} - \mathcal{BK})$ becomes the wanted eigenvalues `poles`, scipy's [`signal.place_poles`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.place_poles.html) package is utilised. This package simply returns the gain matrix given the poles, $\mathcal{A}$ and $\mathcal{B}$.
# In[33]:
# Redefining the initial rod angle in such a way
# that it starts out by falling
x1_0 = 0.2
# Defining the setpoints. I.e. the values of the
# state vector needed for stabilasion
# x0 can be choosed freely, we choose -0.2
setpoints = [-0.2,
0,
0,
0]
# Defining the control matrices A and B
A = np.array([[0, 0, 1, 0],
[0, 0, 0, 1],
[0, -m*g/M, -mu/M, 0],
[0, (m+M)*g/(M*l), mu/(M*l), 0]])
B = np.array([[0],
[0],
[1/M],
[-1/(M*l)]])
# Specifying wanted eigenvalues of the matrix (A - B*K)
# Subscript PG stands for Proportional Gain
poles_PG = [-1.3, -1.4, -1.5, -1.6]
# Get the full state feedback bunch object
fsf = signal.place_poles(A, B, poles_PG)
# Extract the gain matrix
K_PG = fsf.gain_matrix
def controller(z, K):
""" Given the current values of the state vactor, z
and the gain matrix K, it returns the current control
input, u.
"""
offset = z - setpoints
u = -np.dot(K, offset)
return u[0]
def integrateControlled(K, pushForce, pushTime):
""" This is the integration process implemented earlier,
but with controll input added to the force. The possibility
for a 'push' on the pendulum is also added.
"""
# Re-initialising matrices
RHSargs = np.array([m, M, l, g, mu, u_0])
# Z = [(x0_0, x0_1, ..., x0_n), (x1_0, x1_1, ..., x1_n), (x2, ...,), (x3, ...,)]
Z = np.zeros((4, n))
Z[:, 0] = np.array([x0_0, x1_0, x2_0, x3_0])
# Array containing the external forces at each time step
extForce = np.zeros((2, n))
extForce[0, 0] = u_0
# Initialising matrix containing the kinetic and potential energy of the system
energy = np.zeros((2, n)) # energy = [[T_0 ... T_n], [U_0, ... U_n]]
energy[:, 0] = addEnergy([x0_0, x1_0, x2_0, x3_0])
bar = progressbar.ProgressBar()
for i in bar(range(n-1)):
Z[:, i + 1] = RK4Step(Z[:, i], timeStep, RHS, RHSargs)
# "Push"
# Non-continuously increasing/reducing the angular velocity
if i == pushTime and i>0:
Z[3, i + 1] += pushForce
print("Pushed!")
noise = np.random.uniform(-0.01, 0.01)
extForce[1, i +1] = noise
# Calculate the control variable
force = controller(Z[:, i + 1], K)
# Store the force, u
extForce[0, i + 1] = force
# Adding the total force and updating the parameters
f = noise + force
RHSargs[5] = f
# Adding energy terms
energy[:, i + 1] = addEnergy(Z[:, i + 1])
return Z, extForce, energy
Z_PG, extForce_PG, energy_PG = integrateControlled(K_PG, 0, 0)
# In[34]:
plotData(Z_PG, extForce_PG, energy_PG, appliedForce=True)
# And the pendulum is balanced! The state vector elements oscillates around the setpoints before stabilising. The initial points are on positive slopes in phase-space, which indicates an unstable initial position. At the final point the the slope is negative (at $\theta = 0$) which is made possible by the control force.
# ### Linear Quadratic Regulator (LQR)
# We have now been able to stabilise the pendulum on its upright equilibrium, but while doing so we choose some random negative eigenvalues (poles) of $(\mathcal{A} - \mathcal{BK})$. The eigenvalues can be adjusted even more negative, resulting in a more powerful regulation. However, in an engineering situation, one will have a finite maximum aplicable force $u$, other physical limitations, and may want some of the coordinates to stabilise quicker than the others. Thus, it would be attractive to optimise the control. The *cost function* [[4]](#rsc)
#
# \begin{equation}
# J(u) = \int^{\infty}_0 \big(\textbf{x}^{T}\mathcal{Q}\textbf{x} + u^{T}Ru)dt
# \label{CostFunction}
# \end{equation}
#
# is a sum of the deviations of the state vector elements from the set points and the applied force. By minimising this function with weighing factors supplied by the designer ($\mathcal{Q}$ and $R$), one can find the poles which minimise the undesired deviations. The value of the function can be regarded as a penalty. If the system is not stabilised quickly, the first term is going to be large. If the controller spends a lot of energy stabilising, the second term will be large. Say e.g. we dislike having a high angular velocity on our system, and have a beefy motor with cheap electricity, the weighing factors may be defined
#
# \begin{equation}
# \mathcal{Q} =
# \begin{bmatrix} 1 & 0 & 0 & 0 \\
# 0 & 1 & 0 & 0 \\
# 0 & 0 & 1 & 0 \\
# 0 & 0 & 0 & 10
# \end{bmatrix} , \qquad R = 0.1
# \end{equation}
#
# The python package `control` contains a function [`lqr`](https://python-control.readthedocs.io/en/0.8.2/generated/control.lqr.html) which optimises the cost function for us, and simply return the gain matrix $\mathcal{K}$ corresponding to our weighing factors. It has the same functionality as the MATLAB function `lqr` [[5]](#rsc).
# In[47]:
# Weighing factor for the state vector
Q = np.array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 10]])
# Weighing factor for the external force
R = 0.1
# Find the gain matrix and poles using lqr-function
K_LQR, s, poles_LQR = control.lqr(A, B, Q, R)
# Integrate and retrieve the data
Z_LQR, extForce_LQR, energy_LQR = integrateControlled(K_LQR, 0, 0)
plotData(Z_LQR, extForce_LQR, energy_LQR, appliedForce=True)
# The code
#
# generateAnimation(Z_PG, Z_LQR, "lqr_controlled.gif")
#
# generates the animation below. See [appendix C](#apndxC) for the definition of the function.
#
# ![controlled](https://www.numfys.net/media/notebooks/images/lqr_controlled-kopi.gif)
# From the plots and amimation above we see a great improvement in stabilisation time. The angular velocity reaches the set point after two seconds while the controller spends a longer time stabilising the position and velocity of the cart, reflecting the weighing factors. There is also a lot less movement in the system with LQR. This is a direct result of "punishing" deviations.
#
# Let's give the pendulum a little "push" after 4 seconds and see what happens. With a time step of 0.01 seconds, the push is timed at iteration number 400.
# In[48]:
Z_LQR_pushed, extForce_LQR_pushed, energy_LQR_pushed = integrateControlled(K_LQR, .5, 400)
plotData(Z_LQR_pushed, extForce_LQR_pushed, energy_LQR_pushed, appliedForce=True)
# We see that the pendulum is quickly re-stabilised after the push which dramatically (discontinuously) alter the slope of the phase-space path.
# ___
#
#
# ## Resources and Further Reading
# [1] Prof. Alan V. Oppenheim, Demonstration from lecture notes 26, *Feedback example: The Inverted Pendulum*, from the MIT course *Signals and Systems*. Assessed January 31st 2019. [Click here for Lecture notes](https://ocw.mit.edu/resources/res-6-007-signals-and-systems-spring-2011/lecture-notes/MITRES_6_007S11_lec26.pdf). [Click here for video lecture](https://www.youtube.com/watch?v=D3bblng-Kcc).
#
# [2] Packard, Polo, Horowitz, 2002, [*Jacobien Linearizations, equilibrium points*](https://www.cds.caltech.edu/~murray/courses/cds101/fa02/caltech/pph02-ch19-23.pdf), from Dynamic Systems and feedback. Assessed March 8th.
#
# [3] Davis, [*9.3. Equilibrium: Stable or Unstable?*](https://web.ma.utexas.edu/users/davis/375/popecol/lec9/equilib.html), Department of mathematics, University of Texas at Austin. Assessed March 8th.
#
# [4] [Wikipedia](https://en.wikipedia.org/wiki/Linear%E2%80%93quadratic_regulator) has a nice description of LQR and briefly illuminates its application potentials.
#
# [5] MathWorks, [documentation of the function `lqr`](https://www.mathworks.com/help/control/ref/lqr.html).
#
# [6] Prof. Jerrold D. Marsden, [*1 Lecture: Lagrangian Mechanics*](http://www.cds.caltech.edu/archive/help/uploads/wiki/files/212/Mechanics_140a_old.pdf), Caltech, November 21st 2006. Assessed February 4th 2019.
# ___
#
#
# ## Appendix A: Derivations of Equations of motion
#
# The following equations of motion correspond to the system in [figure 2](#img2). Firstly, an expression for the kinetic energy and the potential energy, $T$ and $U$, of the system is found. Then, from the Lagrangian $L = T - U$, the equations of motion are computed using the Euler-Lagrange equation
#
# \begin{equation}
# \frac{d}{dt} \frac{\partial L}{\partial \dot{q}_i} - \frac{\partial L}{\partial q_i} = 0 \text{.}
# \label{EL}
# \end{equation}
#
# Since the rod is massless, the kinetic energy of the system is the sum of the kinetic energy of the cart and the bead,
#
# $$ T = \frac{1}{2}M v^{2}_c + \frac{1}{2}m v^{2}_b .$$
#
# The cart is fixed in the y-axis,
#
# $$ v^2_c = \dot{x}_c^2 + \dot{y}_c^2 = \dot{x}_c^2 .$$
#
# Using the angle of the bead, $\theta$ relative to the y-axis,
#
# \begin{equation}
# \begin{aligned}
# x_b &= x_c + l \sin \theta \\
# y_b &= l\cos \theta \\
# \rightarrow \dot{x}_b &= \dot{x}_c +\dot{\theta}l \cos \theta \\
# \dot{y}_b &= -\dot{\theta} l \sin \theta \\
# \rightarrow v^2_b &= \dot{x}^2_c + \dot{\theta}^2 l^2 + 2\dot{x}_c \dot{\theta}l \cos \theta
# \end{aligned}
# \end{equation}
#
# Thus, with the potential energy
#
# $$ U = mgl\cos \theta ,$$
#
# and rewriting $x_c \rightarrow x$, the Lagrangian becomes
#
# \begin{equation}
# L = \frac{1}{2} \dot{x}^2(m + M) + \frac{1}{2}m\dot{\theta}^2l^2 + m\dot{x}\dot{\theta}l\cos\theta - mgl\cos\theta
# \label{L}
# \end{equation}
#
# Applying \eqref{EL} to the generalized coordinates $q_i = \theta$, we get
#
# \begin{equation}
# \ddot{\theta} = \frac{g}{l}\sin\theta - \frac{\ddot{x}}{l}\cos\theta .
# \label{I}
# \end{equation}
#
# The second generalized coordinate $q_i = x$ is involved in the external force $u$ and the non-conservative force friction. Thus, the Euler-Lagrange equation is replaced by the Lagrange-d'Alambert principle, which is equivalent to the Euler-Lagrange equations with external force [[6]](#rsc):
#
# \begin{equation}
# \frac{d}{dt} \frac{\partial L}{\partial \dot{q}_i} - \frac{\partial L}{\partial q_i} = F_{ext}(q, \dot{q}) \text{.}
# \label{ELEF}
# \end{equation}
#
# This yields the equation of motion
#
# \begin{equation}
# \ddot{x} = \frac{1}{m+M}\big [ ml\big(\dot{\theta}^2\sin\theta - \ddot{\theta}\cos\theta\big) - \mu\dot{x} + u \big ],
# \label{II}
# \end{equation}
#
# where $-\mu\dot{x}$ is an approximation of the friction force on the system ($\mu$ is the friction coefficient), and $u$ is the external force applied to the cart.
#
# On matrix form \eqref{I} and \eqref{II} can be written
#
# \begin{equation}
# \begin{bmatrix} m+M & ml\cos\theta \\
# \cos\theta & l
# \end{bmatrix}
# \begin{bmatrix} \ddot{x} \\
# \ddot{\theta}
# \end{bmatrix} =
# \begin{bmatrix} ml\dot{\theta}^2\sin\theta - \mu\dot{x} \\
# g\sin\theta
# \end{bmatrix} +
# \begin{bmatrix} 1 \\
# 0
# \end{bmatrix} u .
# \end{equation}
#
# Finally, by defining
#
# \begin{equation}
# \Lambda(\theta) =
# \begin{bmatrix} m+M & ml\cos\theta \\
# \cos\theta & l
# \end{bmatrix}
# \end{equation}
#
# the resulting equations of motion becomes
#
# \begin{equation}
# \begin{bmatrix} \ddot{x} \\
# \ddot{\theta}
# \end{bmatrix} = \Lambda(\theta)^{-1}
# \begin{bmatrix} ml\dot{\theta}^2\sin\theta - \mu\dot{x} \\
# g\sin\theta
# \end{bmatrix} +
# \begin{bmatrix} 1 \\
# 0
# \end{bmatrix} u ,
# \label{EQM}
# \end{equation}
#
# where $\Lambda(\theta)^{-1}$ is the inverse of $\Lambda(\theta)$,
#
# \begin{equation}
# \Lambda(\theta)^{-1} = \frac{1}{l(M+m\sin^2\theta)}
# \begin{bmatrix} l & -ml\cos\theta \\
# -\cos\theta & m+M
# \end{bmatrix}.
# \end{equation}
#
#
#
# ## Appendix B - Definition of `plotData`
# In[17]:
def relative(data):
"""Returns the values of 'data' relative to the absolute maximum value of 'data'."""
return data/np.amax(np.absolute(data))
def angle(data):
"""Maps the values of data on to the the interval [1, -1]"""
copy = np.zeros(data.shape)
for i in range(len(data)):
if data[i] <= np.pi:
copy[i] = (np.pi - data[i])/np.pi
else:
copy[i] = -(data[i] - np.pi)/np.pi
return copy
# In[46]:
def plotData(Z, extForce, energy, appliedForce):
""" This function plots the results in time-evolution, and phase-space plots.
It differ between two functionalities, given by the boolean 'appliedForce'.
Arguments:
Z 4xN array. The state vector component-values at each timestep
extForce 2xN array. The value of external force and noise at each timestep
energy 2xN array. The kinetic and potential energy at each timestep
appliedForce boolean.
"""
# Defining an array of the time in seconds at each step
time = np.arange(0, n*timeStep, timeStep)
totalF = extForce[0] + extForce[1]
# Plots time evolution of the external force and every state-vector-component if true
if appliedForce:
plt.figure(figsize=(12, 6), dpi=200)
plt.subplot(2, 2, (1, 3))
plt.title("Time evolution of general coordinates")
plt.plot(time, np.zeros(n), 'b--',label='Setpoint', linewidth=1)
plt.plot(time, [setpoints[0]/np.amax(np.absolute(Z[0])) for i in range(n)],
'--', color="black", linewidth=1, label=r"Setpoint for $x$")
plt.plot(time, relative(Z[0]), label=r'$x$')
plt.plot(time, relative(Z[1]), label=r"$\theta$")
plt.plot(time, relative(Z[2]), label=r"$\dot{x}$")
plt.plot(time, relative(Z[3]), label=r"$\dot{\theta}$")
plt.plot(time, relative(extForce[0]), label=r"$u$")
plt.xlabel('time, sec')
plt.legend(loc='best')
plt.grid(linestyle=':')
# Plots time evolution of the cart position and rod angle only if appliedForce == false.
else:
plt.figure(figsize=(12, 6), dpi=200)
plt.subplot(2, 2, (1, 3))
plt.title("Time evolution of general coordinates")
plt.plot(time, relative(Z[0]), label=r'$x$')
plt.plot(time, angle(Z[1]), label=r"$\theta$")
plt.xlabel('time, A.U')
plt.legend(loc='best')
plt.grid(linestyle=':')
# Independent of appliedForce.
# Generate phase-space plot of the coordinate theta.
plt.subplot(222)
plt.title("Rod angle phase-space plot")
plt.plot(Z[1], Z[3])
plt.plot(Z[1][-1], Z[3][-1], 'o', label="Final point")
plt.plot(Z[1][0], Z[3][0], '*', label="Initial point")
plt.xlabel(r'$\theta$ [rad]')
plt.ylabel(r'$\dot{\theta}$ [rad/s]')
plt.legend(loc='best')
plt.grid(linestyle=":")
# Generates phase-space plot of the coordinate x.
plt.subplot(224)
plt.title("Cart position phase-space plot")
plt.plot(Z[0], Z[2])
plt.plot(Z[0][-1], Z[2][-1], 'o', label="Final point")
plt.plot(Z[0][0], Z[2][0], '*', label="Initial point")
plt.xlabel(r'$x$ [m]')
plt.ylabel(r'$\dot{x}$ [m/s]')
plt.legend(loc='best')
plt.grid(linestyle=":")
# Adjusts horisontal seperation between subplots.
plt.subplots_adjust(hspace=0.4)
if appliedForce == False:
# Plots the loss of total energy as a function of time
plt.figure(figsize=(12, 6), dpi=200)
plt.subplot(121)
plt.title("Loss of total energy due to friction")
# Minimum total energy of the system
minima = m*g*l*np.cos(np.pi)
# The y-axis displays enrgy percentage relative to initial total energy
plt.plot(time, (np.sum(energy, 0) - minima)/(np.sum(energy[:,0], 0) - minima)*100)
plt.xlabel("time, sec")
plt.ylabel('Percentage of initial total energy, %')
plt.grid(linestyle=":")
# Plots the kinetic and potential energy of the system as a function of time.
plt.subplot(122)
plt.title("Kinetic and potential energy of the system")
plt.xlabel("time, sec")
plt.ylabel("energy, J")
plt.plot(time, energy[0], label="T")
plt.plot(time, energy[1], label="U")
plt.legend(loc='best')
plt.grid(linestyle=":")
plt.show()
#
# ## Appendix C - Definition of `generateAnimation` and
# ## `generateAnimationWithPhaseSpace`
# In[3]:
def cartX(x_1):
""" Given the value of x_1, i.e. the position
of the cart, returns the x-coordinates of the
cart's corners."""
return np.array([x_1 - l/8, x_1 + l/8, x_1 + l/8, x_1 - l/8, x_1 - l/8])
# In[41]:
def generateAnimation(Z, Z2, name):
""" This function generates two animations of the system as a function of time,
from the two results, 'Z' and 'Z2'. The two animations are plottet underneath each other.
Arguments:
Z 4xN array. State vector component-values at each timestep
Z2 4xN array. State vector component-values at each timestep
name string. Name of the animation file.
"""
# Defining an array of the time in seconds at each step
timeValues = np.arange(0, n*timeStep, timeStep)
X = Z[0] + l*np.sin(Z[1]) # x-coordinates of bead
Y = l*np.cos(Z[1]) # y-coordinates of bead
cartY = np.array([l/16, l/16, -l/16, -l/16, l/16]) # y-coordinates of cart corners
X2 = Z2[0] + l*np.sin(Z2[1]) # x-coordinates of bead 2
Y2 = l*np.cos(Z2[1]) # y-coordinates of bead 2
cartY2 = np.array([l/16, l/16, -l/16, -l/16, l/16]) # y-coordinates of cart corners 2
fig = plt.figure(figsize=(figSizeX, figSizeY), dpi=200)
# Animation/result/pendulum number one
plt.subplot(211)
# Axis limitations
xMin = np.min(X) - 1.5*l
xMax = np.max(X) + 1.5*l
# Adjust the y-limitations such that a circle looks like a circle.
# i.e. no scaling.
xDomain = xMax - xMin
yDomain = xDomain * 0.5*figSizeY/figSizeX
plt.xlim(xMin, xMax)
plt.ylim(-0.5*yDomain, 0.5*yDomain)
# Defining the different elements in the animation
surface, = plt.plot([xMin, xMax], [-l/16, -l/16], color='black', linewidth=1) # The surface
tail, = plt.plot(X[0], Y[0], '--', color="blue") # Previous position of the pendulum bead
cart, = plt.plot(cartX(Z[0, 0]), cartY, color="red") # The cart
rod, = plt.plot([Z[0, 0], X[0]], [0, Y[0]], color="black") # The massless pendulum rod of length l
bead, = plt.plot(X[0], Y[0], 'o', color="black", ms=4) # The pendulum bead
text = plt.text(xMax-0.1, 0.5*yDomain - 0.1, r'$t = %.2f$s'%(timeValues[0]), # Text box with elapsed time
{'color': 'k', 'fontsize': 10, 'ha': 'center', 'va': 'center',
'bbox': dict(boxstyle='round', fc='w', ec='k', pad=0.2)})
plt.xlabel('x, m')
plt.ylabel('y, m')
plt.title("Proportional Gain controller")
# Animation/results/pendulum number two
plt.subplot(212)
# Axis limitations
xMin2 = np.min(X2) - 3*l
xMax2 = np.max(X2) + 3*l
xDomain2 = xMax2 - xMin2
yDomain2 = xDomain2 * 0.5*figSizeY/figSizeX
plt.xlim(xMin2, xMax2)
plt.ylim(-0.5*yDomain2, 0.5*yDomain2)
# Defining the different elements in the animation
surface2, = plt.plot([xMin2, xMax2], [-l/16, -l/16], color='black', linewidth=1) # The surface
tail2, = plt.plot(X2[0], Y2[0], '--', color="blue") # Previous position of the pendulum bead
cart2, = plt.plot(cartX(Z2[0, 0]), cartY2, color="red") # The cart
rod2, = plt.plot([Z2[0, 0], X2[0]], [0, Y2[0]], color="black") # The massless pendulum rod of length l
bead2, = plt.plot(X2[0], Y2[0], 'o', color="black", ms=4) # The pendulum bead
text2 = plt.text(xMax2-0.1, 0.5*yDomain2 - 0.1, r'$t = %.2f$s'%(timeValues[0]), # Text box with elapsed time
{'color': 'k', 'fontsize': 10, 'ha': 'center', 'va': 'center',
'bbox': dict(boxstyle='round', fc='w', ec='k', pad=0.2)})
plt.xlabel('x, m')
plt.ylabel('y, m')
plt.title("Linear Quadratic Regulator")
plt.subplots_adjust(hspace=0.3)
# Calculate the number of frames
FPS = 30
framesNum = int(FPS*duration)
dataPointsPerFrame = int(n/framesNum)
# Animation function. This is called sequentially
def animate(j):
time = j*dataPointsPerFrame
# Pendulum 1
tail.set_xdata(X[:time])
tail.set_ydata(Y[:time])
cart.set_xdata(cartX(Z[0, time]))
cart.set_ydata(cartY)
rod.set_xdata([Z[0, time], X[time]])
rod.set_ydata([0, Y[time]])
bead.set_xdata(X[time])
bead.set_ydata(Y[time])
text.set_text(r'$t = %.2f$s'%(timeValues[time]))
# Pendulum 2
tail2.set_xdata(X2[:time])
tail2.set_ydata(Y2[:time])
cart2.set_xdata(cartX(Z2[0, time]))
cart2.set_ydata(cartY2)
rod2.set_xdata([Z2[0, time], X2[time]])
rod2.set_ydata([0, Y2[time]])
bead2.set_xdata(X2[time])
bead2.set_ydata(Y2[time])
text2.set_text(r'$t = %.2f$s'%(timeValues[time]))
# Create animation
anim = animation.FuncAnimation(fig, animate, frames=framesNum)
# Save animation.
# If this don't work for you, try using another writer
# (ffmpeg, mencoder, imagemagick), or another file extension
# (.mp4, .gif, .ogg, .ogv, .avi etc.). Make sure that you
# have the codec and the writer installed on your system.
anim.save(name, writer='pillow', fps=FPS)
# Close plot
plt.close(anim._fig)
# Display the animation
with open(name, 'rb') as file:
display(Image(file.read()))
# In[30]:
def generateAnimationWithPhaseSpace(Z, name):
""" This function generates an animation consisting of a plot of the pendulum
and phase-space plot of the two generalised coordinates x and theta, as a
functino of time.
Arguments:
Z 4xN array. State vector component-values at each timestep
name string. Name of the animation file.
"""
X = Z[0] + l*np.sin(Z[1]) # x-coordinates of bead
Y = l*np.cos(Z[1]) # y-coordinates of bead
cartY = np.array([l/16, l/16, -l/16, -l/16, l/16]) # y-coordinates of cart corners
fig = plt.figure(figsize=(figSizeX, figSizeY), dpi=200)
# Axis limitations
plt.subplot(2, 2, (1, 3))
xMin = np.min(X) - l/2
xMax = np.max(X) + l/2
# Adjust the y-limitations such that a circle looks like a circle.
# i.e. no scaling.
xDomain = xMax - xMin
yDomain = xDomain*2*figSizeY/figSizeX
plt.xlim(xMin, xMax)
plt.ylim(-0.5*yDomain, 0.5*yDomain)
# Defining the different elements in the animation
surface, = plt.plot([xMin, xMax], [-l/16, -l/16], color='black', linewidth=1) # The surface
tail, = plt.plot(X[0], Y[0], '--', color="blue") # Previous position of the pendulum bead
cart, = plt.plot(cartX(Z[0, 0]), cartY, color="red") # The cart
rod, = plt.plot([Z[0, 0], X[0]], [0, Y[0]], color="black") # The massless pendulum rod of length l
bead, = plt.plot(X[0], Y[0], 'o', color="black", ms=4) # The pendulum bead
plt.xlabel('x')
plt.ylabel('y')
plt.title("Inverted pendulum on cart")
# Phase-space plot of the rod angle theta
plt.subplot(222)
plt.title("Rod angle in phase-space")
anglePath, = plt.plot(Z[1], Z[3])
angleFinal, = plt.plot(Z[1, -1], Z[3, -1], 'o', label="Final point")
angleCurrent, = plt.plot(Z[1, 0], Z[3, 0], '*', label="Current point")
plt.xlabel(r'$\theta$ [rad]')
plt.ylabel(r'$\dot{\theta}$ [rad/s]')
plt.legend(loc='best')
plt.grid(linestyle=":")
# Phase-space plot of the cart position x.
plt.subplot(224)
plt.title("Cart position in phase-space")
cartPath, = plt.plot(Z[0], Z[2])
cartFinal, = plt.plot(Z[0, -1], Z[2, -1], 'o', label="Final point")
cartCurrent, = plt.plot(Z[0, 0], Z[2, 0], '*', label="Current point")
plt.xlabel(r'$x$ [m]')
plt.ylabel(r'$\dot{x}$ [m/s]')
plt.legend(loc='best')
plt.grid(linestyle=":")
# Calculate the number of frames
FPS = 30
framesNum = int(FPS*duration)
dataPointsPerFrame = int(n/framesNum)
# Animation function. This is called sequentially
def animate(j):
time = j*dataPointsPerFrame
# Updating each element every time
tail.set_xdata(X[:time])
tail.set_ydata(Y[:time])
cart.set_xdata(cartX(Z[0, time]))
cart.set_ydata(cartY)
rod.set_xdata([Z[0, time], X[time]])
rod.set_ydata([0, Y[time]])
bead.set_xdata(X[time])
bead.set_ydata(Y[time])
angleCurrent.set_xdata(Z[1, time])
angleCurrent.set_ydata(Z[3, time])
anglePath.set_xdata(Z[1, :time])
anglePath.set_ydata(Z[3, :time])
cartCurrent.set_xdata(Z[0, time])
cartCurrent.set_ydata(Z[2, time])
cartPath.set_xdata(Z[0, :time])
cartPath.set_ydata(Z[2, :time])
# Create animation
anim = animation.FuncAnimation(fig, animate, frames=framesNum)
# Save animation.
# If this don't work for you, try using another writer
# (ffmpeg, mencoder, imagemagick), or another file extension
# (.mp4, .gif, .ogg, .ogv, .avi etc.). Make sure that you
# have the codec and the writer installed on your system.
anim.save(name, writer='pillow', fps=FPS)
# Close plot
plt.close(anim._fig)
# Display the animation
with open(name, 'rb') as file:
display(Image(file.read()))
# In[ ]: