Constrained Double Pendulum¶
This notebook models a double pendulum as two indipendent rods joined by a hinge treated as constraint. We use the same logic as in the getting started notebook.
[1]:
# Project and symbolic tools
import heyoka as hy
import quivira as qv
# Numerics and visualization
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
Matplotlib is building the font cache; this may take a moment.
Generalized Coordinates¶
For each rod we use its center position and orientation:
[2]:
# Generalized coordinates: center positions and orientations of the two rods.
x0, y0, theta0, x1, y1, theta1 = hy.make_vars("x0", "y0", "theta0", "x1", "y1", "theta1")
dx0, dy0, dtheta0, dx1, dy1, dtheta1 = hy.make_vars("dx0", "dy0", "dtheta0", "dx1", "dy1", "dtheta1")
q = [x0, y0, theta0, x1, y1, theta1]
qd = [dx0, dy0, dtheta0, dx1, dy1, dtheta1]
Kinetic Energy¶
We model each rod as uniform, so its moment of inertia about its center is \(mL^2/12\). The kinetic energy includes center translation and rod rotation:
[3]:
L0 = 1.0
L1 = 1.0
m0 = 1.0
m1 = 1.0
g = 9.81
# Kinetic and potential energies for the two rigid rods. Each rod contributes
# center-of-mass translational energy and rotational energy about its center.
T = 0.5 * m0 * (qd[0] ** 2 + qd[1] ** 2)
T += 0.5 * m0 * (L0 ** 2) * (qd[2] ** 2) / 12
T += 0.5 * m1 * (qd[3] ** 2 + qd[4] ** 2)
T += 0.5 * m1 * (L1 ** 2) * (qd[5] ** 2) / 12
Potential Energy¶
With gravity acting in the negative \(y\) direction, the potential is
[4]:
V = m0 * g * q[1] + m1 * g * q[4]
Lagrangian¶
[5]:
L = T - V
Holonomic constraints.¶
The first rod is pinned at the origin, so its upper endpoint stays fixed. The second rod starts at the lower endpoint of the first. These connections give four holonomic constraints:
[6]:
# Top attachment: the first rod starts at the origin and is anchored there.
# The second rod connects to the end of the first rod.
F = np.asarray([
q[0] - 0.5 * L0 * hy.cos(q[2]),
q[1] - 0.5 * L0 * hy.sin(q[2]),
q[3] - q[0] - 0.5 * L0 * hy.cos(q[2]) - 0.5 * L1 * hy.cos(q[5]),
q[4] - q[1] - 0.5 * L0 * hy.sin(q[2]) - 0.5 * L1 * hy.sin(q[5]),
], dtype=object)
EOM¶
We assemble the equations of motion for the bead using Quivira amazing capabilities.
lagrange_eom returns the first-order ODE system and, when requested, the Lagrange multipliers. The multipliers follow the constraint order and represent the joint reactions.
[7]:
ode_system, lambda_expr = qv.lagrange_eom(L, F, q, qd, return_multipliers=True)
Taylor Integrator¶
We can now use the assembled equations of motion to instantiate heyoka Taylor adaptive integrator
[8]:
ta = hy.taylor_adaptive(ode_system)
Initial conditions¶
We release the rods from rest at these angles:
The rod centers are calculated from the constraints, so the initial state starts on the allowed geometry.
[9]:
# Start from a sideways double-pendulum configuration with zero initial speeds.
theta0 = 1.1
theta1 = -1.5
x0 = 0.5 * L0 * np.cos(theta0)
y0 = 0.5 * L0 * np.sin(theta0)
x1 = x0 + 0.5 * L0 * np.cos(theta0) + 0.5 * L1 * np.cos(theta1)
y1 = y0 + 0.5 * L0 * np.sin(theta0) + 0.5 * L1 * np.sin(theta1)
state0 = [x0, y0, theta0, x1, y1, theta1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
Simulation¶
We set the integrator to the constraint-consistent initial state and sample the motion at 401 times from 0 to 8. The final item returned by propagate_grid is the array of sampled states.
[10]:
ta.time = 0.0
ta.state[:] = state0
t_end = 8.
tgrid = np.linspace(0.0, t_end, 401)
solution = ta.propagate_grid(tgrid)[-1]
Animate the motion.
[11]:
# Draw each rod as a segment centered at its coordinates.
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_aspect('equal')
ax.grid(True)
ax.set_xlabel('x')
ax.set_ylabel('y')
frame_extent = L0 + L1 + 0.25
ax.set_xlim(-frame_extent, frame_extent)
ax.set_ylim(-frame_extent, frame_extent)
rod0_line, = ax.plot([], [], 'o-', lw=3, color='tab:blue')
rod1_line, = ax.plot([], [], 'o-', lw=3, color='tab:orange')
trail, = ax.plot([], [], '-', lw=1, alpha=0.6, color='black')
# Find rod endpoints from the center, angle, and length.
def rod_points(state, rod_length):
x, y, theta = state[0], state[1], state[2]
u = np.array([np.cos(theta), np.sin(theta)])
start = np.array([x, y]) - 0.5 * rod_length * u
end = np.array([x, y]) + 0.5 * rod_length * u
return start, end
# Reset the plotted objects before the animation starts.
def init_anim():
rod0_line.set_data([], [])
rod1_line.set_data([], [])
trail.set_data([], [])
return rod0_line, rod1_line, trail
# Move the rods and extend the trail at each frame.
def update_anim(frame):
state = solution[frame]
x0, y0, theta0, x1, y1, theta1 = state[:6]
start0, end0 = rod_points(np.array([x0, y0, theta0]), L0)
start1, end1 = rod_points(np.array([x1, y1, theta1]), L1)
rod0_line.set_data([start0[0], end0[0]], [start0[1], end0[1]])
rod1_line.set_data([start1[0], end1[0]], [start1[1], end1[1]])
tip_x = solution[: frame + 1, 3] + 0.5 * L1 * np.cos(solution[: frame + 1, 5])
tip_y = solution[: frame + 1, 4] + 0.5 * L1 * np.sin(solution[: frame + 1, 5])
trail.set_data(tip_x, tip_y)
return rod0_line, rod1_line, trail
anim = FuncAnimation(
fig,
update_anim,
frames=len(solution),
interval=20,
blit=False,
init_func=init_anim,
)
display(HTML(anim.to_jshtml()))
plt.close(fig)