codexdouble-pendulum-linear-mode-patterns.py
double-pendulum-linear-mode-patterns.py
"""Draw original linear mode patterns and histories for an ideal double pendulum."""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
fig = plt.figure(figsize=(8.32, 5.6), dpi=125)
gs = fig.add_gridspec(2, 2, width_ratios=[1, 2.5], left=0.06, right=0.97,
bottom=0.22, top=0.80, hspace=0.72, wspace=0.38)
tau = np.linspace(0, 10, 901)
amplitude = 0.06
colors = ["#246ba0", "#bd5127"]
for row, sign in enumerate([1, -1]):
shape = np.array([1, sign * np.sqrt(2)])
omega = np.sqrt(2 - sign * np.sqrt(2))
pattern = fig.add_subplot(gs[row, 0])
theta = amplitude * shape
rods = np.column_stack((np.sin(theta), -np.cos(theta)))
points = np.vstack(([0, 0], np.cumsum(rods, axis=0)))
points[:, 0] *= 4
pattern.plot([0, 0], [0, -2], "--", color="#bbbbbb", lw=1)
pattern.plot(points[:, 0], points[:, 1], color="#555555", lw=3)
pattern.scatter(*points[0], color="#333333", s=32, zorder=3)
for i in range(2):
pattern.scatter(*points[i + 1], s=90, color=colors[i], zorder=3)
direction = np.sign(points[i + 1, 0])
pattern.annotate("", (points[i + 1, 0] + direction * 0.07, points[i + 1, 1]),
(-0.04 if points[i + 1, 0] > 0 else 0.04, points[i + 1, 1]),
arrowprops={"arrowstyle": "->", "lw": 1.6, "color": colors[i]})
pattern.text(-0.62, -0.69, r"$\theta_1=A$", fontsize=10, color=colors[0])
pattern.text(-0.62, -1.73,
r"$\theta_2=\sqrt{2}A$" if sign == 1 else r"$\theta_2=-\sqrt{2}A$",
fontsize=10, color=colors[1])
pattern.set(xlim=(-0.68, 0.83), ylim=(-2.14, 0.18))
pattern.axis("off")
pattern.set_title("Low mode: in phase" if sign == 1 else "High mode: opposed", fontsize=12)
history = fig.add_subplot(gs[row, 1])
for i in range(2):
history.plot(tau, shape[i] * np.cos(omega * tau), color=colors[i],
lw=2, linestyle="-" if i == 0 else "--",
label=rf"$\theta_{i + 1}/A$")
history.axhline(0, color="#999999", lw=0.7)
history.set(xlim=(0, 10), ylim=(-1.65, 1.65), yticks=[-1, 0, 1],
ylabel="Angle / A")
history.set_title(r"$\omega\sqrt{\ell/g}=\sqrt{2-\sqrt{2}}$" if sign == 1 else
r"$\omega\sqrt{\ell/g}=\sqrt{2+\sqrt{2}}$", fontsize=13, pad=10)
history.grid(alpha=0.16)
history.spines[["top", "right"]].set_visible(False)
if row == 0:
history.legend(loc="upper right", framealpha=1, ncol=2, fontsize=10)
else:
history.set_xlabel(r"Same dimensionless time in both rows: $\tau=t\sqrt{g/\ell}$")
fig.suptitle("Two patterns, two natural frequencies", y=0.97, fontsize=18)
fig.text(0.5, 0.905, r"Equal point masses, equal rods; $A=0.06$ rad", ha="center", fontsize=12)
fig.text(0.5, 0.035, "Pattern snapshots: horizontal offsets enlarged ×4, heights unchanged.\n"
"Curves are predictions of the linear model, not measurements.",
ha="center", fontsize=10, linespacing=1.5)
fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()