codextorsion-balance-readout.py
torsion-balance-readout.py
"""Original torsion-balance schematic; illustrative geometry, not measured data.
Tested with Python 3.14.4, matplotlib 3.10.7, numpy 2.3.5.
Run from _media: MPLCONFIGDIR=/tmp/codex-wiki-matplotlib python3 ../torsion-balance-readout.py
Writes the matching opaque white PNG basename to the current working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Arc, Circle, Rectangle
import numpy as np
BLUE = '#245a91'
ORANGE = '#b74b16'
GREEN = '#197149'
GRAY = '#6b7280'
def arrow(ax, start, end, color):
ax.annotate('', xy=end, xytext=start, arrowprops={'arrowstyle': '-|>', 'color': color, 'lw': 2.1, 'mutation_scale': 14})
def rotor(ax, beta):
v = np.array([np.cos(beta), np.sin(beta)])
ax.plot([-v[0], v[0]], [-v[1], v[1]], color=BLUE, lw=4)
for sign in (-1, 1):
ax.add_patch(Circle(sign*v, .075, facecolor=BLUE, edgecolor=BLUE))
ax.add_patch(Circle((0, 0), .045, facecolor='black'))
ax.plot([-1.25, 1.35], [0, 0], '--', color=GRAY, lw=1.1, zorder=0)
tangent = np.array([-v[1], v[0]])
for sign in (-1, 1):
start = sign*v
arrow(ax, start, start+sign*.58*tangent, ORANGE)
return v
def main():
plt.rcParams.update({'font.size': 11, 'figure.facecolor': 'white', 'axes.facecolor': 'white'})
fig, axes = plt.subplots(1, 3, figsize=(11.4, 4.7))
fig.subplots_adjust(left=.025, right=.98, top=.77, bottom=.14, wspace=.24)
fig.suptitle('Torsion balance: convert a weak torque into a twist', fontsize=17, y=.97)
fig.text(.5, .88, 'Original ideal schematic • angles exaggerated • no measured data', ha='center', color=GRAY)
for ax in axes:
ax.set_aspect('equal')
ax.axis('off')
ax.set_xlim(-1.55, 1.55)
ax.set_ylim(-.9, 2.05)
ax = axes[0]
ax.set_title('Suspension (side view)', pad=10)
ax.add_patch(Rectangle((-.35, 1.7), .70, .12, color=GRAY))
ax.plot([0, 0], [.1, 1.7], color=GREEN, lw=2.3)
ax.plot([-1, 1], [.1, .1], color=BLUE, lw=4)
ax.add_patch(Circle((-1, .1), .075, color=BLUE))
ax.add_patch(Circle((1, .1), .075, color=BLUE))
ax.text(.1, 1.68, 'top attachment', va='top', fontsize=10)
ax.text(.16, .95, 'thin elastic fiber', color=GREEN, fontsize=10)
ax.text(0, -.12, 'suspended body', color=BLUE, ha='center', fontsize=10)
arrow(ax, (-1.24, 1.02), (-1.24, 1.52), GRAY)
ax.text(-1.13, 1.25, 'vertical\nrotation axis', fontsize=9, va='center')
ax.text(0, -.63, 'Weight is supported;\naxial torque twists the fiber.', ha='center', fontsize=10)
ax = axes[1]
ax.set_title('Deflection (top view)', pad=10)
beta = np.deg2rad(25)
v = rotor(ax, beta)
ax.add_patch(Arc((0, 0), .94, .94, theta1=0, theta2=25, color=GREEN, lw=1.8))
ax.text(.65, .13, r'$\beta=\theta$', color=GREEN)
ax.text(1.05, .96, r'$F$', color=ORANGE, fontsize=13)
ax.text(-1.19, -.74, r'$F$', color=ORANGE, fontsize=13)
ax.text(-.12, .35, r'$a$', color=BLUE)
ax.text(0, 1.55, r'Top fixed: $\alpha=0$', ha='center')
ax.text(0, 1.12, r'Applied $\tau=\kappa\beta>0$', color=ORANGE, ha='center')
ax.text(0, -.79, r'Fiber torque $=-\kappa\beta$', color=GREEN, ha='center')
ax = axes[2]
ax.set_title('Null readout (top view)', pad=10)
rotor(ax, 0)
ax.text(1.08, .60, r'$F$', color=ORANGE, fontsize=13)
ax.text(-1.2, -.72, r'$F$', color=ORANGE, fontsize=13)
ax.text(0, .88, r'Body held at $\beta=0$', ha='center', color=BLUE)
# The dial is shown separately above the body, not as another physical rod.
center = np.array([0., 1.56])
ax.add_patch(Circle(center, .29, fill=False, edgecolor=GRAY, lw=1.1))
ax.plot([0, .25], [1.56, 1.56], '--', color=GRAY, lw=1)
end = center+.25*np.array([np.cos(-beta), np.sin(-beta)])
ax.plot([center[0], end[0]], [center[1], end[1]], color=GREEN, lw=2)
ax.add_patch(Arc(center, .41, .41, theta1=-25, theta2=0, color=GREEN, lw=1.6))
ax.text(.4, 1.53, r'$\alpha<0$', color=GREEN, va='center')
ax.text(-.39, 1.88, 'top dial', color=GRAY, fontsize=9)
ax.text(0, -.79, r'Twist $\theta=\beta-\alpha=-\alpha$', color=GREEN, ha='center')
fig.text(.5, .035, r'Positive torque is counterclockwise; at equilibrium $\tau_{\mathrm{ext}}=\kappa(\beta-\alpha)$.', ha='center', fontsize=11)
fig.savefig(Path(__file__).with_suffix('.png').name, dpi=110, facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()