Simulating The Double Pendulum Using Lagrangian Formulation

The double pendulum is a classic example of a chaotic system in physics. It consists of two pendulums of \( L_1 \) and \( L_2 \) attached end to end, and its motion is highly sensitive to initial conditions. In this project, we will simulate the motion of a double pendulum using the Lagrangian formulation and visualize the results using Python's Matplotlib library.

The position of the masses can be described using the angles \( \theta_1 \) and \( \theta_2 \), which represent the angles of the first and second pendulums with respect to the vertical axis. Then the position of the masses can be expressed as: \[ x_1 = L_1 \sin\theta_1, \quad y_1 = -L_1 \cos\theta_1, \quad x_2 = x_1 + L_2 \sin\theta_2, \quad y_2 = y_1 - L_2 \cos\theta_2 \] The Kinetic Energy of the system is given by: \[ T = \frac{1}{2} m_1 ( \dot{x}_1^2 + \dot{y}_1^2 ) + \frac{1}{2} m_2 ( \dot{x}_2^2 + \dot{y}_2^2 ) \] The Potential Energy of the system is given by: \[ V = m_1 g y_1 + m_2 g y_2 = - m_1 g L_1 \cos\theta_1 - m_2 g (L_1 \cos\theta_1 + L_2 \cos\theta_2) \] The Lagrangian \( \mathcal{L} \) is defined as the difference between the Kinetic and Potential Energies: \[ \mathcal{L}(\theta_1, \theta_2, \dot{\theta}_1, \dot{\theta}_2) = T - V \] From Euler-Lagrange equations, we derive the equations of motion for the double pendulum: \[ \frac{d}{dt} \left( \frac{\partial \mathcal{L}}{\partial \dot{\theta}_i} \right) - \frac{\partial \mathcal{L}}{\partial \theta_i} = 0, \quad i = 1, 2 \]

Necessary Packages and Paths


    import numpy as np
    from scipy.integrate import solve_ivp
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation
    

Initial Setup


    g = 9.81          # gravitational acceleration (m/s^2)

    L1 = 0.75          # length of first rod (m)
    L2 = 1.25          # length of second rod (m)

    m1 = 2.0          # mass of first bob (kg)
    m2 = 1.0          # mass of second bob (kg)

    theta1_0 = np.pi/2       # initial angle of first pendulum (rad)
    theta2_0 = np.pi/2 + 0.1 # initial angle of second pendulum (rad)

    omega1_0 = 0.0           # initial angular velocity of first pendulum (rad/s)
    omega2_0 = 0.0           # initial angular velocity of second pendulum (rad/s)
    

Simulation Setup


    t_start = 0.0
    t_end   = 20.0          
    dt      = 0.01
    t_eval  = np.arange(t_start, t_end, dt)
    

Simulation


    def double_pendulum_rhs(t, y, L1, L2, m1, m2, g):
        theta1, omega1, theta2, omega2 = y

        delta = theta2 - theta1

        den1 = (m1 + m2)*L1 - m2*L1*np.cos(delta)*np.cos(delta)
        den2 = (L2/L1) * den1

        # Angular accelerations
        num1 = (m2*L1*omega1*omega1*np.sin(delta)*np.cos(delta) +
                m2*g*np.sin(theta2)*np.cos(delta) +
                m2*L2*omega2*omega2*np.sin(delta) -
                (m1 + m2)*g*np.sin(theta1))

        alpha1 = num1 / den1

        num2 = (-m2*L2*omega2*omega2*np.sin(delta)*np.cos(delta) +
                (m1 + m2)*(g*np.sin(theta1)*np.cos(delta) -
                        L1*omega1*omega1*np.sin(delta) -
                        g*np.sin(theta2)))

        alpha2 = num2 / den2

        return [omega1, alpha1, omega2, alpha2]
    

Solving ODEs


    y0 = [theta1_0, omega1_0, theta2_0, omega2_0]

    sol = solve_ivp(
        double_pendulum_rhs,
        t_span=(t_start, t_end),
        y0=y0,
        t_eval=t_eval,
        args=(L1, L2, m1, m2, g),
        rtol=1e-9,
        atol=1e-9
    )

    theta1 = sol.y[0]
    omega1 = sol.y[1]
    theta2 = sol.y[2]
    omega2 = sol.y[3]
    

Position


    x1 = L1 * np.sin(theta1)
    y1 = -L1 * np.cos(theta1)

    x2 = x1 + L2 * np.sin(theta2)
    y2 = y1 - L2 * np.cos(theta2)
    

Compute Angular Acceleration


    def compute_angular_accelerations(theta1, omega1, theta2, omega2):
        alpha1 = np.zeros_like(theta1)
        alpha2 = np.zeros_like(theta2)
        for i in range(len(theta1)):
            _, a1, _, a2 = double_pendulum_rhs(
                t_eval[i],
                [theta1[i], omega1[i], theta2[i], omega2[i]],
                L1, L2, m1, m2, g
            )
            alpha1[i] = a1
            alpha2[i] = a2
        return alpha1, alpha2

    alpha1, alpha2 = compute_angular_accelerations(theta1, omega1, theta2, omega2)
    

Compute Linear Velocity


    vx1 = L1 * omega1 * np.cos(theta1)
    vy1 = L1 * omega1 * np.sin(theta1)

    vx2 = vx1 + L2 * omega2 * np.cos(theta2)
    vy2 = vy1 + L2 * omega2 * np.sin(theta2)
    

Final Values


    def numerical_derivative(f, t):
        df = np.zeros_like(f)
        df[1:-1] = (f[2:] - f[:-2]) / (t[2:] - t[:-2])
        df[0] = (f[1] - f[0]) / (t[1] - t[0])
        df[-1] = (f[-1] - f[-2]) / (t[-1] - t[-2])
        return df

    ax1 = numerical_derivative(vx1, t_eval)
    ay1 = numerical_derivative(vy1, t_eval)
    ax2 = numerical_derivative(vx2, t_eval)
    ay2 = numerical_derivative(vy2, t_eval)

    x1_mag = x1
    v1_mag = np.sqrt(vx1**2 + vy1**2)
    a1_mag = np.sqrt(ax1**2 + ay1**2)

    x2_mag = x2
    v2_mag = np.sqrt(vx2**2 + vy2**2)
    a2_mag = np.sqrt(ax2**2 + ay2**2)
    

Animation


    fig_anim, ax_anim = plt.subplots(figsize=(5, 5))
    ax_anim.set_aspect('equal')
    max_len = L1 + L2
    ax_anim.set_xlim(-max_len*1.1, max_len*1.1)
    ax_anim.set_ylim(-max_len*1.1, max_len*1.1)
    ax_anim.set_xlabel('x ($m$)')
    ax_anim.set_ylabel('y ($m$)')

    line, = ax_anim.plot([], [], 'o-', lw=2, color = '#363636')
    trace, = ax_anim.plot([], [], '-', lw=1, alpha=0.5, color = '#DB4437')
    time_text = ax_anim.text(0.05, 0.9, '', transform=ax_anim.transAxes)

    # Keep a trace of the second bob
    trace_x, trace_y = [], []

    def init():
        line.set_data([], [])
        trace.set_data([], [])
        time_text.set_text('')
        return line, trace, time_text

    def update(frame):
        # Current positions
        this_x = [0, x1[frame], x2[frame]]
        this_y = [0, y1[frame], y2[frame]]
        line.set_data(this_x, this_y)

        trace_x.append(x2[frame])
        trace_y.append(y2[frame])
        trace.set_data(trace_x, trace_y)

        time_text.set_text(f't = {t_eval[frame]:.2f} s')
        return line, trace, time_text

    anim = FuncAnimation(
        fig_anim,
        update,
        frames=len(t_eval),
        init_func=init,
        interval=dt*1000,
        blit=True
    )
    anim.save('double_pendulum.mp4', fps=30, dpi=150)
    

Output Animation

Plotting


    fig, axes = plt.subplots(3, 2, figsize=(10, 8), sharex=True)

    # Bob 1
    axes[0, 0].plot(t_eval, x1_mag, label='Bob 1', color = '#4285F4')
    axes[0, 0].set_ylabel('x ($m$)')
    axes[0, 0].legend(loc='upper center', bbox_to_anchor=(0.5, 1.2), ncol=1, frameon=False)
    axes[0, 0].grid(alpha=0.2)
    axes[0, 0].set_xlim([t_eval[0], t_eval[-1]])

    axes[1, 0].plot(t_eval, v1_mag, color = '#4285F4')
    axes[1, 0].set_ylabel('v ($ms^{-1}$)')
    axes[1, 0].grid(alpha=0.2)
    axes[1, 0].set_xlim([t_eval[0], t_eval[-1]])

    axes[2, 0].plot(t_eval, a1_mag, color = '#4285F4')
    axes[2, 0].set_ylabel('a ($ms^{-2}$)')
    axes[2, 0].set_xlabel('time ($s$)')
    axes[2, 0].grid(alpha=0.2)
    axes[2, 0].set_xlim([t_eval[0], t_eval[-1]])

    # Bob 2
    axes[0, 1].plot(t_eval, x2_mag, color = '#DB4437', label='Bob 2')
    axes[0, 1].set_ylabel('x ($m$)')
    axes[0, 1].legend(loc='upper center', bbox_to_anchor=(0.5, 1.2), ncol=1, frameon=False)
    axes[0, 1].grid(alpha=0.2)
    axes[0, 1].set_xlim([t_eval[0], t_eval[-1]])

    axes[1, 1].plot(t_eval, v2_mag, color = '#DB4437')
    axes[1, 1].set_ylabel('v ($ms^{-1}$)')
    axes[1, 1].grid(alpha=0.2)
    axes[1, 1].set_xlim([t_eval[0], t_eval[-1]])

    axes[2, 1].plot(t_eval, a2_mag, color = '#DB4437')
    axes[2, 1].set_ylabel('a ($ms^{-2}$)')
    axes[2, 1].set_xlabel('time ($s$)')
    axes[2, 1].grid(alpha=0.2)
    axes[2, 1].set_xlim([t_eval[0], t_eval[-1]])
    plt.savefig('DoublePendulum.png', dpi = 200)
    plt.show()
    

Simulation Result

Double Pendulum Kinematics Plots
Double Pendulum Kinematics Plots