Homework 3 solutions#

We want to integrate the simple pendulum without the small angle approximation and compare different integration schemes

import numpy as np
import matplotlib.pyplot as plt
class SimplePendulum:
    """ manage and integrate a simple pendulum """

    def __init__(self, theta0, g=9.81, L=9.81, method="Euler"):
        """we'll take theta0 in degrees and assume that the angular
        velocity is initially 0"""
        
        # initial condition
        self.theta0 = np.radians(theta0)

        self.g = g
        self.L = L

    def energy(self, theta_vec, omega_vec):
        """ given a solution, return the energy (per unit mass) """
        return 0.5 * self.L**2 * omega_vec**2 - self.g * self.L * np.cos(theta_vec)

    def period(self):
        """ return an estimate of the period, up to the theta**2 term """
        return 2.0 * np.pi * np.sqrt(self.L / self.g) * (1.0 + self.theta0**2 / 16.0)

    def rhs(self, theta, omega):
        """ equations of motion for a pendulum
              dtheta/dt = omega
              domega/dt = - (g/L) sin theta """

        return np.array([omega, -(self.g / self.L) * np.sin(theta)])

    def integrate_euler(self, dt, tmax):
        """ integrate the equations of motion using Euler's method """

        # initial conditions
        t = 0.0

        t_s = [t]
        theta_s = [self.theta0]
        omega_s = [0.0]

        while t < tmax:

            # initial state
            theta = theta_s[-1]
            omega = omega_s[-1]
            
            # get the RHS
            thetadot, omegadot = self.rhs(theta, omega)

            # advance
            theta_new = theta + dt * thetadot
            omega_new = omega + dt * omegadot

            t += dt

            # store
            t_s.append(t)
            theta_s.append(theta_new)
            omega_s.append(omega_new)

        return np.array(t_s), np.array(theta_s), np.array(omega_s)
        
    def integrate_ec(self, dt, tmax):
        """ integrate the equations of motion using the Euler-Cromer method """

        # initial conditions
        t = 0.0

        t_s = [t]
        theta_s = [self.theta0]
        omega_s = [0.0]

        while t < tmax:

            # initial state
            theta = theta_s[-1]
            omega = omega_s[-1]
            
            # get the RHS
            thetadot, omegadot = self.rhs(theta, omega)

            # advance
            omega_new = omega + dt * omegadot
            theta_new = theta + dt * omega_new

            t += dt

            # store
            t_s.append(t)
            theta_s.append(theta_new)
            omega_s.append(omega_new)

        return np.array(t_s), np.array(theta_s), np.array(omega_s)
    
    def integrate_vv(self, dt, tmax):
        """ integrate the equations of motion using velocity Verlet method """

        # initial conditions
        t = 0.0

        t_s = [t]
        theta_s = [self.theta0]
        omega_s = [0.0]

        while t < tmax:

            # initial state
            theta = theta_s[-1]
            omega = omega_s[-1]
            
            # get the RHS at time-level n
            thetadot, omegadot = self.rhs(theta, omega)

            omega_half = omega + 0.5 * dt * omegadot
                
            theta_new = theta + dt * omega_half

            # get the RHS with the updated theta -- omega doesn't matter
            # here, since we only need thetadot and omega doesn't affect
            # that.
            _, omegadot_new = self.rhs(theta_new, omega)
            omega_new = omega_half + 0.5 * dt * omegadot_new

            t += dt

            # store
            t_s.append(t)
            theta_s.append(theta_new)
            omega_s.append(omega_new)

        return np.array(t_s), np.array(theta_s), np.array(omega_s)

Let’s try it out

Case 1: \(\theta_0 = 10^\circ\)#

# initial angle in degrees -- the class converts to radians
theta0 = 10

p10 = SimplePendulum(theta0)
period = p10.period()
print(f"{period=}")
period=6.295147605281553

Euler’s method

# fixed timestep
dt = 0.1
tmax = 10 * period
t_euler, theta_euler, omega_euler = p10.integrate_euler(dt, tmax)
fig, ax = plt.subplots()
ax.plot(t_euler, theta_euler, label="Euler")

ax.set_xlabel("t [s]")
ax.set_ylabel(r"$\theta$")

ax.grid()
../_images/b3229802323f497f9ab34962f45985ae36f8bda807e621b697ee733fc5268d96.png

Now let’s do Euler-Cromer

t_ec, theta_ec, omega_ec = p10.integrate_ec(dt, tmax)

ax.plot(t_ec, theta_ec, label="Euler-Cromer")
ax.legend()
fig
../_images/cce152759054b860f3e17ea49ace340b17ea4e8a26cce2313c56aefebb5a2b18.png

finally velocity-Verlet

t_vv, theta_vv, omega_vv = p10.integrate_vv(dt, tmax)

ax.plot(t_vv, theta_vv, label="velocity-Verlet")
ax.legend()
fig
../_images/38e46d72c15244830f4d2550408af952af45b210d54a327d32463ceb828b0de3.png

We see that the Euler-Cromer and velocity-Verlet solutions look the same, and the amplitude of the pendulum does not change noticeably with time—as expected, since there is no damping in the system.

Let’s look at energy

E_euler = p10.energy(theta_euler, omega_euler)
E_ec = p10.energy(theta_ec, omega_ec)
E_vv = p10.energy(theta_vv, omega_vv)

fig = plt.figure()

ax1 = fig.add_subplot(211)

ax1.plot(t_euler, E_euler, label="Euler")
ax1.legend()

ax2 = fig.add_subplot(212)
ax2.plot(t_ec, E_ec, label="Euler-Cromer")
ax2.plot(t_vv, E_vv, label="velocity-Verlet")

ax2.legend()
<matplotlib.legend.Legend at 0x7f99a8631ed0>
../_images/7f4b899f1e48a184ef96410fc732545cd9b24f81df61572d691d5ecf07d3c9e7.png

Here we see that the energy conservation in Euler is really bad.

Case 2: \(\theta_0 = 100^\circ\)#

theta0 = 100

p100 = SimplePendulum(theta0)
period = p100.period()
print(f"{period=}")
period=7.479415117376338

Notice that the period here is very different than the classic small-angle approximation period.

# fixed timestep
dt = 0.1
tmax = 10 * period
t_euler, theta_euler, omega_euler = p100.integrate_euler(dt, tmax)
t_ec, theta_ec, omega_ec = p100.integrate_ec(dt, tmax)
t_vv, theta_vv, omega_vv = p100.integrate_vv(dt, tmax)
fig, ax = plt.subplots()

ax.plot(t_euler, theta_euler, label="Euler")
ax.plot(t_ec, theta_ec, label="Euler-Cromer")
ax.plot(t_vv, theta_vv, label="velocity-Verlet")

ax.legend()
ax.set_xlabel("t [s]")
ax.set_ylabel(r"$\theta$")

ax.set_ylim(-8, 3)
ax.grid()
../_images/f6babc36175b27040288f6b38cdd354bdb218e198a46c098642b8dc8600c573b.png

Again, the Euler-Cromer and velocity-Verlet methods track nicely

E_euler = p100.energy(theta_euler, omega_euler)
E_ec = p100.energy(theta_ec, omega_ec)
E_vv = p100.energy(theta_vv, omega_vv)

fig = plt.figure()

ax1 = fig.add_subplot(211)

ax1.plot(t_euler, E_euler, label="Euler")
ax1.legend()

ax2 = fig.add_subplot(212)
ax2.plot(t_ec, E_ec, label="Euler-Cromer")
ax2.plot(t_vv, E_vv, label="velocity-Verlet")

ax2.legend()
<matplotlib.legend.Legend at 0x7f99a85b3160>
../_images/c1ef224f9566c54dcee0a587eb434f4a1c36d2de2cc069df5059006897f0f22a.png