Non Linear Trajectory Optimization:Acrobat or Double Pendulum

casadi
Code
Python
Author

Arpan Pallar

Published

August 4, 2023

Double pendulum have highly non linear dymanics. Thats why making controlling them and making them reach a desired state is an interesting control problem. In this blog we ll try to make a double pendulum stand straight by controlling a torque only at the mid joint using multiple shooting method. The result would look something like this.

Why it is called the Acrobat? Because Acrobats does not have enough torque at their wrist. So they swing their body to reach vertical position above the pole

Dynamics Equation

For a double pendulum with only torque \(u\) at the middle joint the dynamics is given by

\(M(q)\ddot q + C(q,\dot q)\dot q= \tau_g(q)+Bu\)

where

$ M(q) =

\[\begin{matrix}I_1+I_2+m_2l_1^2+2m_2l_1l_2c_2 & I_2+m_2l_1l_2c_2\\I_2+m_2l_1l_2c_2 & 1_2 \end{matrix}\]

$

\(C(q,\dot q) = \begin{matrix} -2m_2l_1l_2s_2\dot q_2 & -m_2l_1l_2s_2\dot q_2 \\ m_2l_1l_2s_2\dot q_1 & 0\end{matrix}\)

\(\tau_g(q) = \begin{matrix} -m_1gl_1s_1-m_2g(l_1s_1+l_2s_{1+2}) \\ -m_2gl_2s_{1+2} \end{matrix}\)

\(B = \begin{matrix} 0 \\ 1\end{matrix}\)

where

\(q = [\theta_1,\theta_2]\)

\(s1 = sin(\theta_1) ,\ s_{1+2} = sin(\theta_1+\theta_2)\)

we can code this in python like

def deriv2(y,u):
    theta1 = y[0]
    theta2 = y[1]
    y1_dot = y[2]
    y2_dot = y[3]
    g = 9.81
    s1 = np.sin(theta1)


    M = np.array([[I1+I2+m2*l1**2+2*m2*l1*l2*np.cos(theta2),I2+m2*l1*l2*np.cos(theta2)],
         [I2+m2*l1*l2*np.cos(theta2),I2]])
    print(M.shape)
    C = np.array([[-2*m2*l1*l2*np.sin(theta2)*y2_dot, -m2*l1*l2*np.sin(theta2)*y2_dot],
         [m2*l1*l2*np.sin(theta2)*y1_dot,0]])
    print(C.shape)
    T = np.array([[-m1*g*l1*s1-m2*g*(l1*s1+l2*np.sin(theta1+theta2))],[-m2*g*l2*np.sin(theta1+theta2)]])
    print(T.shape)
    B = np.array([[0],[1]])
    print(B.shape)
    d2y = np.matmul(np.linalg.inv(M),T+np.matmul(B,u)-np.matmul(C,[[y1_dot],[y2_dot]]))
    return np.array([y1_dot,y2_dot,d2y[0][0],d2y[1][0]])

Optimal Equation Formulation

we use multiple shooting method that means we put state and the control input u as decision variable and use the dynamics equation as constraints

we want to minimize the cost function

 L =  0.5*(u)**2 + 100*(MX.cos(theta1)+1)**2+100*(MX.cos(theta2)+1)**2+10*dtheta1**2+10*dtheta2**2

subject to

\(\dot x-f(x,u)=0\)

or if we discretize using RK4 at \(t_n\) we get

\(X_{n+1} -g(X_{n},u)=0\)

Code

we use CASADI as the program to solve this NLP. I recommend watching this tutorial video from the creater of Casadi,Joris Gillis to know how to use it https://www.youtube.com/watch?v=ANicKS8gKdM

first create the dynamics in casadi matrix form

def deriv(y,t,u,l):
    theta1 = y[0]
    theta2 = y[1]
    y1_dot = y[2]
    y2_dot = y[3]
    g = 9.81
    s1 = MX.sin(theta1)

    M = MX(2,2)
    M[0,0] = I1+I2+m2*l1**2+2*m2*l1*l2*MX.cos(theta2)
    M[0,1] = I2+m2*l1*l2*MX.cos(theta2)
    M[1,0] = I2+m2*l1*l2*MX.cos(theta2)
    M[1,1] = I2
    
    C = MX(2,2)
    C[0,0] = -2*m2*l1*l2*MX.sin(theta2)*y2_dot
    C[0,1] = -m2*l1*l2*MX.sin(theta2)*y2_dot
    C[1,0] = m2*l1*l2*MX.sin(theta2)*y1_dot
    C[1,1] = 0

    T = MX(2,1)
    T[0,0] = -m1*g*l1*s1-m2*g*(l1*s1+l2*MX.sin(theta1+theta2))
    T[1,0] = -m2*g*l2*MX.sin(theta1+theta2)
   
    B = MX(2,1)
    B[0,0] = 0
    B[1,0] = 1
    
    y_dot = MX(2,1)
    y_dot[0,0]=y1_dot 
    y_dot[1,0] = y2_dot

    d2y = solve(M,T+mtimes(B,u)-mtimes(C,y_dot))
    return y1_dot,y2_dot,d2y[0][0],d2y[1][0]

Then define and set the optimal control problem

x_k = state at Kth time step; 4x1 matrix

u_k = torque at Kth time step; 1x1 matrix

lbw = lower limit on decision variable

ubw = upper limit on decision variable

w0 = initial guess on the state

lbg = constraints lower limit

ubg = constraints upper limit

w=[]
w0 = []
lbw = []
ubw = []
J = 0
g=[]
lbg = []
ubg = []

# "Lift" initial conditions
Xk = MX.sym('X0', 4)
w += [Xk]
lbw += [0.1, 0.0,0,0]
ubw += [0.1, 0.0,0,0]
w0 += [0.1, 0.0,0,0]

# Formulate the NLP
for k in range(N):
    # New NLP variable for the control
    Uk = MX.sym('U_' + str(k))
    w   += [Uk]
    lbw += [-inf]
    ubw += [inf]
    w0  += [0]

    # Integrate till the end of the interval
    Fk = F(x0=Xk, u=Uk)
    Xk_end = Fk['xf']
    J=J+Fk['qf']

    # New NLP variable for state at end of interval
    Xk = MX.sym('X_' + str(k+1), 4)
    w   += [Xk]
    lbw += [-inf, -inf,-inf,-inf]
    ubw += [ inf,inf,inf,inf]
    w0  += [0,0,0,0]

    # Add equality constraint
    g   += [Xk_end-Xk]
    lbg += [0, 0,0,0]
    ubg += [0, 0,0,0]

# Create an NLP solver
prob = {'f': J, 'x': vertcat(*w), 'g': vertcat(*g)}
solver = nlpsol('solver', 'ipopt', prob)


sol = solver(x0=w0, lbx=lbw, ubx=ubw, lbg=lbg, ubg=ubg)
w_opt = sol['x'].full().flatten()

# Plot the solution
x1_opt = w_opt[0::5]
x2_opt = w_opt[1::5]
x3_opt = w_opt[2::5]
x4_opt = w_opt[3::5]
u_opt = w_opt[4::5]

Animate optimal states

fig = plt.figure(figsize=(8.33,6.25),dpi=72)
ax = fig.add_subplot(111)
y = [math.pi/3,math.pi/2,0,0]
x1 = math.sin(y[0])
y1 = - math.cos(y[0])
x2 = x1+math.sin(y[1])
y2 = y1-math.cos(y[1])
line, = ax.plot([0,x1,x2],[0,y1,y2],lw=2,c='k')
r=0.05
c1 = Circle((x1,y1),r,fc='b',ec='b',zorder=10)
c2 = Circle((x2,y2),r,fc='b',ec='b',zorder=10)
circle1 = ax.add_patch(c1)
circle2  = ax.add_patch(c2)
ns=20
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)



def animate(i):
    x1 = math.sin(y_track[i,0])
    y1 = - math.cos(y_track[i,0])
    x2 = x1+math.sin(y_track[i,1])
    y2 = y1-math.cos(y_track[i,1])
    line.set_data([0,x1,x2],[0,y1,y2])
    circle1.set_center((x1,y1))
    circle2.set_center((x2,y2))
    
y_init = [0.1,0,0,0]
X = y_init
t = [0,0.1]

y_track=np.array([y_init for i in range(100)])
print(y_track.shape)
for i in range(0,99):
    y_track[i,:] = [x1_opt[i],x2_opt[i],x3_opt[i],x4_opt[i]]
    

ani = animation.FuncAnimation(fig, animate,frames=99,interval=100)
#plt.show()
FFwriter = animation.FFMpegWriter(fps=10)
ani.save('animation.mp4', writer = FFwriter)