Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Lagrange’s equations of motion (1788)#
Joseph-Louis Lagrange’s Mécanique analytique (1788) contains no figures. It derives the motion of any system from two scalar functions, the kinetic energy \(T\) and the potential energy \(V\), written in whatever coordinates \(q\) fix its configuration. With \(L = T - V\),
and the constraint forces, such as the tension in a pendulum rod, never
appear. lagrange_accelerations()
evaluates these equations numerically from a Python function for \(L\),
and simulate_lagrangian()
integrates them. The example runs a double pendulum, checks it against the
hand-derived equations and the conservation of energy, and linearises it to
the small-oscillation eigenproblem Lagrange also set out.
import matplotlib.pyplot as plt
import numpy as np
from engineeringkit.mechanical_engineering import lagrange_accelerations, normal_modes, simulate_lagrangian
The double pendulum#
Two point masses on rigid massless rods, angles \(\theta_1, \theta_2\) from the downward vertical.
m1, m2, l1, l2, g = 1.0, 1.0, 1.0, 1.0, 9.81
def lagrangian(q, v):
T = 0.5 * (m1 + m2) * l1**2 * v[0] ** 2 + 0.5 * m2 * l2**2 * v[1] ** 2 + m2 * l1 * l2 * v[0] * v[1] * np.cos(q[0] - q[1])
V = -(m1 + m2) * g * l1 * np.cos(q[0]) - m2 * g * l2 * np.cos(q[1])
return T - V
q, v = np.array([2.0, 2.5]), np.array([0.0, 0.0])
d = q[0] - q[1]
H = np.array([[(m1 + m2) * l1**2, m2 * l1 * l2 * np.cos(d)], [m2 * l1 * l2 * np.cos(d), m2 * l2**2]])
rhs = [
-m2 * l1 * l2 * v[1] ** 2 * np.sin(d) - (m1 + m2) * g * l1 * np.sin(q[0]),
m2 * l1 * l2 * v[0] ** 2 * np.sin(d) - m2 * g * l2 * np.sin(q[1]),
]
by_hand = np.linalg.solve(H, rhs)
print("accelerations from L:", np.round(lagrange_accelerations(lagrangian, q, v), 6).tolist(), " by hand:", np.round(by_hand, 6).tolist())
assert np.allclose(lagrange_accelerations(lagrangian, q, v), by_hand, rtol=1e-6)
accelerations from L: [-10.316811, 3.182843] by hand: [-10.316811, 3.182842]
Chaotic motion that conserves energy#
Released from high up, the motion is chaotic, but \(T + V\) stays constant; \(L(q, 0) = -V\).
t = np.linspace(0, 10, 2001)
run = simulate_lagrangian(lagrangian, q, v, t)
energy = np.array([lagrangian(a, b) - 2 * lagrangian(a, 0 * b) for a, b in zip(run.displacement, run.velocity)])
print(f"energy drift over 10 s: {np.ptp(energy) / abs(energy[0]):.1e} of the total")
assert np.ptp(energy) < 1e-5 * abs(energy[0])
energy drift over 10 s: 5.2e-07 of the total
Small oscillations#
Near the hanging position \(T \approx \tfrac12\dot q^T M\dot q\) and \(V \approx \tfrac12 q^T K q\), so Lagrange’s equations become \(M\ddot q + Kq = 0\). Started in a normal mode, the pendulum returns to its start after one period.
M = np.array([[(m1 + m2) * l1**2, m2 * l1 * l2], [m2 * l1 * l2, m2 * l2**2]])
K = np.diag([(m1 + m2) * g * l1, m2 * g * l2])
modes = normal_modes(M, K)
print("small-oscillation periods [s]:", np.round(2 * np.pi / modes.frequencies, 4).tolist())
for shape, w in zip(modes.mode_shapes, modes.frequencies):
small = simulate_lagrangian(lagrangian, 1e-3 * shape, [0.0, 0.0], np.linspace(0, 2 * np.pi / w, 101))
assert np.allclose(small.displacement[-1], small.displacement[0], atol=1e-8)
x1, y1 = l1 * np.sin(run.displacement[:, 0]), -l1 * np.cos(run.displacement[:, 0])
x2, y2 = x1 + l2 * np.sin(run.displacement[:, 1]), y1 - l2 * np.cos(run.displacement[:, 1])
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.plot(x2, y2, lw=0.5)
ax1.plot([0, x1[0], x2[0]], [0, y1[0], y2[0]], "ko-")
ax1.set(aspect="equal", xlabel="x [m]", ylabel="y [m]", title="Path of the lower mass")
run.plot(ax=ax2)
ax2.set(title="Generalised coordinates [rad]")
fig.tight_layout()
plt.show()
![Path of the lower mass, Generalised coordinates [rad]](../../../../_images/sphx_glr_plot_03_lagrange_equations_001.png)
small-oscillation periods [s]: [2.6211, 1.0857]
Total running time of the script: (0 minutes 0.691 seconds)