codexangular-momentum-radius-change.py
angular-momentum-radius-change.py
"""Generate angular-momentum-radius-change.png in the current working directory.
Use: make _media/angular-momentum-radius-change.png
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Two 1 kg point masses, no applied torque; radial endpoint velocities vanish.
Transient radial kinetic energy is omitted from the endpoint comparison.
"""
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle
def main():
plt.rcParams.update({"font.size": 13, "savefig.facecolor": "white"})
total_mass = 2.0
radii = np.array([1.0, 0.5])
inertia = total_mass * radii**2
angular_momentum = inertia[0] * 1.0
omega = angular_momentum / inertia
energy = inertia * omega**2 / 2
work = energy[1] - energy[0]
assert np.allclose(inertia, [2, 0.5])
assert np.allclose(omega, [1, 4])
assert np.allclose(inertia * omega, [2, 2])
assert np.allclose(energy, [1, 4]) and np.isclose(work, 3)
fig, axes = plt.subplots(1, 2, figsize=(9.2, 6.0), facecolor="white")
fig.subplots_adjust(left=0.06, right=0.94, bottom=0.41, top=0.81, wspace=0.32)
fig.suptitle("Pull inward: faster rotation, the same angular momentum", fontsize=17, y=0.97)
fig.text(0.5, 0.89, "Two point masses of 1 kg each; total mass M = 2 kg. Top view, common spatial scale.",
ha="center", fontsize=12)
for i, (ax, radius, angular_speed, inertial, kinetic) in enumerate(zip(axes, radii, omega, inertia, energy)):
color = ["#2469a0", "#b94d36"][i]
ax.set(xlim=(-1.4, 1.4), ylim=(-1.15, 1.15), aspect="equal")
ax.axis("off")
ax.add_patch(Circle((0, 0), radius, fill=False, ls="--", ec="#b8c1cc", lw=1.3))
ax.plot([-radius, radius], [0, 0], color="#d0d4d8", lw=1.3)
ax.scatter([-radius, radius], [0, 0], s=160, color=color, zorder=4)
ax.scatter([0], [0], s=28, color="#263341", zorder=4)
ax.text(-0.07, 0.13, "O", ha="center", fontsize=11)
for sign in [-1, 1]:
ax.annotate("", (sign * radius, sign * 0.42 * radius * angular_speed), (sign * radius, 0),
arrowprops={"arrowstyle": "-|>", "color": color, "lw": 2.4})
ax.annotate("", (radius, -0.23), (0, -0.23),
arrowprops={"arrowstyle": "<->", "color": "#666666"})
ax.text(radius / 2, -0.47, rf"$r={radius:g}$ m", ha="center", fontsize=12)
ax.set_title(["Before", "After"][i], color=color, fontsize=15, pad=8)
summary = (rf"$I=Mr^2={inertial:g}$ kg m$^2$" + "\n" +
rf"$\omega={angular_speed:g}$ rad/s" + "\n" +
rf"$L_z=I\omega={angular_momentum:g}$ kg m$^2$/s" + "\n" +
rf"$K=\frac{{1}}{{2}}I\omega^2={kinetic:g}$ J")
ax.text(0.5, -0.09, summary, transform=ax.transAxes, ha="center", va="top", fontsize=13,
linespacing=1.6, color=color)
fig.text(0.5, 0.125, r"$\Delta K=4-1=+3$ J: the pulling apparatus supplies $+3$ J of net work.",
ha="center", fontsize=13, bbox={"facecolor": "#f4f5f7", "edgecolor": "none", "pad": 8})
fig.text(0.5, 0.035, "Radial velocity is zero at both endpoints. Transient radial kinetic energy is not shown.\n"
"Circles show endpoint radii; arrows show tangential velocities, not the inward path.",
ha="center", fontsize=10, linespacing=1.5)
fig.savefig("angular-momentum-radius-change.png", dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()