codexmechanical-energy-oscillation.py
mechanical-energy-oscillation.py
"""Draw mechanical-energy-oscillation.png in the current working directory.
Run from _media; dependencies are declared in the root pyproject.toml.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
The figure is an original calculation, with an opaque white background.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def main():
mass, stiffness, amplitude = 0.5, 2.0, 0.3
omega = np.sqrt(stiffness / mass)
period = 2 * np.pi / omega
energy = stiffness * amplitude**2 / 2
time = np.linspace(0, period, 1201)
position = amplitude * np.cos(omega * time)
velocity = -amplitude * omega * np.sin(omega * time)
potential = stiffness * position**2 / 2
kinetic = mass * velocity**2 / 2
np.testing.assert_allclose([omega, period, energy], [2, np.pi, 0.09])
np.testing.assert_allclose(potential + kinetic, energy, atol=1e-14)
blue, orange, dark = "#1765a8", "#bf5808", "#30363d"
plt.rcParams.update({
"font.size": 11,
"axes.titlesize": 12,
"axes.spines.top": False,
"axes.spines.right": False,
"figure.facecolor": "white",
"axes.facecolor": "white",
"savefig.facecolor": "white",
})
fig, axes = plt.subplots(2, 2, figsize=(10, 7.3))
fig.subplots_adjust(left=0.09, right=0.98, bottom=0.09, top=0.83,
hspace=0.50, wspace=0.33)
fig.suptitle("One oscillator: motion, phase ellipse and energy", y=0.98, fontsize=16)
fig.text(0.5, 0.925,
r"$m=0.5$ kg, $k=2$ N/m, $A=0.3$ m; "
r"$\omega=2$ s$^{-1}$, $P=\pi$ s, $E=0.09$ J",
ha="center", fontsize=12)
fig.text(0.5, 0.875, "Released from rest at x = +0.3 m; no damping or drive",
ha="center", fontsize=11, color=dark)
ticks = np.arange(5) * period / 4
ticklabels = ["0", r"$\pi/4$", r"$\pi/2$", r"$3\pi/4$", r"$\pi$"]
def time_axis(ax):
ax.set_xlim(0, period)
ax.set_xticks(ticks, ticklabels)
ax.set_xlabel("Time t (s)")
ax.grid(alpha=0.18)
ax = axes[0, 0]
ax.plot(time, position, color=blue, lw=2.5)
ax.axhline(0, color="#8a929a", lw=0.8)
ax.scatter(ticks, amplitude * np.cos(omega * ticks), color=dark, s=22, zorder=3)
ax.set(title=r"Position: $x=0.3\cos(2t)$", ylabel="Displacement x (m)",
ylim=(-0.37, 0.37), yticks=[-0.3, 0, 0.3])
time_axis(ax)
ax = axes[0, 1]
ax.plot(time, velocity, color=orange, lw=2.5)
ax.axhline(0, color="#8a929a", lw=0.8)
ax.scatter(ticks, -amplitude * omega * np.sin(omega * ticks),
color=dark, s=22, zorder=3)
ax.set(title=r"Velocity: $v=-0.6\sin(2t)$", ylabel="Velocity v (m/s)",
ylim=(-0.74, 0.74), yticks=[-0.6, 0, 0.6])
time_axis(ax)
ax = axes[1, 0]
ax.plot(position, velocity, color=blue, lw=2.5)
ax.axhline(0, color="#8a929a", lw=0.8)
ax.axvline(0, color="#8a929a", lw=0.8)
ax.scatter(position[::300], velocity[::300], color=dark, s=24, zorder=3)
for phase in [np.pi / 4, 3 * np.pi / 4, 5 * np.pi / 4, 7 * np.pi / 4]:
before, after = phase - 0.10, phase + 0.10
ax.annotate("", xy=(amplitude * np.cos(after), -amplitude * omega * np.sin(after)),
xytext=(amplitude * np.cos(before), -amplitude * omega * np.sin(before)),
arrowprops={"arrowstyle": "->", "color": dark, "lw": 1.8,
"mutation_scale": 16})
labels = [(0.3, 0, "t = 0", (8, 8)),
(0, -0.6, r"$t=\pi/4$", (6, -16)),
(-0.3, 0, r"$t=\pi/2$", (6, 10)),
(0, 0.6, r"$t=3\pi/4$", (6, 10))]
for x, v, label, offset in labels:
ax.annotate(label, (x, v), xytext=offset, textcoords="offset points", fontsize=10)
ax.set(title="Phase ellipse: same energy at every point",
xlabel="Displacement x (m)", ylabel="Velocity v (m/s)",
xlim=(-0.42, 0.42), ylim=(-0.83, 0.83),
xticks=[-0.3, 0, 0.3], yticks=[-0.6, 0, 0.6])
ax.grid(alpha=0.18)
ax = axes[1, 1]
ax.plot(time, potential, color=blue, lw=2.3, label=r"$V=0.09\cos^2(2t)$")
ax.plot(time, kinetic, color=orange, lw=2.3, label=r"$T=0.09\sin^2(2t)$")
ax.axhline(energy, color=dark, lw=1.8, ls="--", label=r"$E=T+V=0.09$ J")
ax.set(title="Energy exchange: two cycles per period", ylabel="Energy (J)",
ylim=(-0.006, 0.15), yticks=[0, 0.045, 0.09])
time_axis(ax)
ax.legend(loc="upper center", frameon=False, fontsize=9)
fig.savefig(Path.cwd() / "mechanical-energy-oscillation.png", dpi=110,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()