codex/gyroscope-wheel-torque-vector-geometry.py

gyroscope-wheel-torque-vector-geometry.py
"""Original fixed-pivot wheel and angular-momentum geometry. Python 3.14.

Run from _media with MPLBACKEND=Agg. Uses matplotlib 3.10.7/numpy 2.3.5.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Arc

BLUE = '#1266a4'
RED = '#b72a30'
GREEN = '#238249'
fig, (ax, top) = plt.subplots(1, 2, figsize=(10, 4.6), facecolor='white')
fig.suptitle('A spinning wheel: force, torque and precession', fontsize=16)
ax.set(xlim=(-.6, 3.6), ylim=(-1.1, 2.6), aspect='equal')
ax.axis('off')
# Side view is the xz plane; +y goes into the page.
pivot = np.array([0., .25])
e = np.array([np.sin(np.pi/3), np.cos(np.pi/3)])
centre = pivot + 2.1*e
ax.plot([-.45, .45], [-.4, -.4], color='gray', lw=6)
ax.plot([0, 0], [-.4, .25], color='gray', lw=5)
ax.plot(*zip(pivot, centre+.45*e), color='gray', lw=4)
ax.add_patch(Ellipse(centre, width=.58, height=1.42, angle=-30,
                     fc='#eef5fa', ec=BLUE, lw=3))
ax.plot(*pivot, 'ko', ms=7)
ax.plot(*centre, 'ko', ms=4)
ax.text(-.08, .07, r'$O$: fixed socket', ha='right', fontsize=11)
ax.text(centre[0]+.1, centre[1]-.2, 'centre of mass', fontsize=11)
def arrow(ax, start, end, color, lw=2.5):
    ax.annotate('', xy=end, xytext=start,
                arrowprops=dict(arrowstyle='-|>', color=color, lw=lw))
arrow(ax, pivot, pivot+np.array([0, 2.1]), 'black', 1.5)
ax.text(-.08, 2.38, r'$+z$', fontsize=12)
arrow(ax, pivot, pivot+np.array([.7, 0]), 'black', 1.5)
ax.text(.72, .12, r'$+x$', fontsize=12)
arrow(ax, pivot+.07*np.array([-e[1], e[0]]), centre+.65*e, BLUE)
ax.text(2.35, 2.04, r'$L_s\hat{\mathbf{e}}$', color=BLUE, fontsize=13)
ax.text(.69, .92, r'$\mathbf{r}=d\hat{\mathbf{e}}$', color=BLUE, rotation=30, fontsize=12)
arrow(ax, centre, centre+np.array([0, -1.45]), RED)
ax.text(centre[0]+.12, -.31, r'$\mathbf{F}_g=-Mg\hat{\mathbf{z}}$', color=RED, fontsize=12)
ax.add_patch(Arc(pivot, .85, .85, theta1=30, theta2=90, color='black'))
ax.text(.22, .77, r'$\theta$', fontsize=13)
ax.plot(.52, 2.2, marker='x', color=GREEN, ms=9, mew=2)
ax.add_patch(plt.Circle((.52, 2.2), .14, fill=False, ec=GREEN, lw=1.5))
ax.text(.78, 2.17, r'$\tau$: into page ($+y$)', color=GREEN, fontsize=11)
ax.set_title('Side view: axle tilted 60° from vertical', fontsize=12)
# View from +z: the circle is the horizontal projection of the L-tip cone.
top.set(xlim=(-1.5, 1.6), ylim=(-1.5, 1.6), aspect='equal')
top.axis('off')
a = np.linspace(0, 2*np.pi, 200)
top.plot(np.cos(a), np.sin(a), '--', color='gray', lw=1.5)
top.plot(0, 0, 'ko', ms=5)
arrow(top, (0, 0), (1, 0), BLUE)
phi = .33
arrow(top, (0, 0), (np.cos(phi), np.sin(phi)), '#80b2d3', 2)
arrow(top, (1, 0), (1, .67), GREEN)
top.text(1.07, .64, r'$\tau\,dt$', color=GREEN, fontsize=13)
top.text(.38, -.17, r'$L_s\sin\theta$', color=BLUE, fontsize=13)
angles = np.linspace(.65, 1.62, 40)
top.plot(1.22*np.cos(angles), 1.22*np.sin(angles), color=GREEN, lw=2)
arrow(top, (1.22*np.cos(angles[-2]), 1.22*np.sin(angles[-2])),
      (1.22*np.cos(angles[-1]), 1.22*np.sin(angles[-1])), GREEN, 2)
top.text(-1.42, 1.23, r'$\Omega>0$: counterclockwise', color=GREEN, fontsize=11)
top.text(0, -1.42, 'Reverse spin → reverse slow precession', ha='center', fontsize=11)
top.set_title(r'Top view: horizontal projection of $\mathbf{L}$', fontsize=12)
fig.text(.5, .03, 'Original schematic • not to scale • spin-dominated angular momentum', ha='center', fontsize=11)
fig.subplots_adjust(left=.07, right=.97, bottom=.15, top=.8, wspace=.25)
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), dpi=130, facecolor='white')
plt.close(fig)