"""Original calculated torsion calibration curves, 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-calibration.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 import numpy as np def main(): plt.rcParams.update({'font.size': 11, 'figure.facecolor': 'white', 'axes.facecolor': 'white'}) kappa, inertia, gamma = 1e-8, 2e-6, .020 omega0 = np.sqrt(kappa/inertia) omegad = np.sqrt(omega0**2-gamma**2) period0, periodd = 2*np.pi/omega0, 2*np.pi/omegad delta = gamma*periodd # Choose phase so t=0 and every integer damped period are true positive maxima. phase = -np.arctan(gamma/omegad) amplitude = .020/np.cos(phase) t = np.linspace(0, 3.1*periodd, 1600) theta = amplitude*np.exp(-gamma*t)*np.cos(omegad*t+phase) envelope = amplitude*np.exp(-gamma*t) fig, axes = plt.subplots(1, 2, figsize=(10.6, 4.9)) fig.subplots_adjust(left=.085, right=.98, bottom=.25, top=.76, wspace=.32) fig.suptitle('Two ways to calibrate a torsion constant', fontsize=17, y=.97) fig.text(.5, .87, r'Toy model, not measured data: $\kappa=10^{-8}$ N m/rad', ha='center', color='#555555') blue, orange, gray = '#245a91', '#b74b16', '#6b7280' ax = axes[0] ax.set_title('Static: slope of known torque vs twist', fontsize=12) angles = np.linspace(-.022, .022, 100) ax.plot(angles*1e3, kappa*angles/1e-10, color=blue, lw=2) pts = np.array([-.020, -.010, 0, .010, .020]) ax.scatter(pts*1e3, kappa*pts/1e-10, color=blue, s=35, zorder=3) ax.axhline(0, color=gray, lw=.7) ax.axvline(0, color=gray, lw=.7) ax.set_xlabel(r'Twist $\theta$ (mrad)') ax.set_ylabel(r'Applied torque ($10^{-10}$ N m)') ax.set_xlim(-24, 24) ax.set_ylim(-2.45, 2.45) ax.annotate(r'20 mrad $\to 2\times10^{-10}$ N m', xy=(20, 2), xytext=(-20, 1.2), fontsize=10, arrowprops={'arrowstyle': '->', 'color': gray}) ax.grid(alpha=.17) ax = axes[1] ax.set_title('Dynamic: period plus viscous decay', fontsize=12) ax.plot(t, 20*np.cos(omega0*t), '--', color=gray, alpha=.6, lw=1, label='no damping') ax.plot(t, theta*1e3, color=orange, lw=2, label='viscous damping') for sign in (-1, 1): ax.plot(t, sign*envelope*1e3, ':', color=orange, lw=1) peaks_t = np.array([0., periodd, 2*periodd]) peaks = .020*np.exp(-gamma*peaks_t) ax.scatter(peaks_t, peaks*1e3, color=orange, s=25, zorder=4) ax.annotate('', xy=(periodd, 16), xytext=(0, 16), arrowprops={'arrowstyle': '<->', 'color': orange}) ax.text(periodd/2, 17.7, r'$T_d=92.64$ s', ha='center', color=orange, fontsize=10) ax.text(122, -17, r'$\delta=\ln(p_n/p_{n+1})=1.853$', fontsize=10, color=orange) ax.set_xlabel('Time (s)') ax.set_ylabel('Twist (mrad)') ax.set_xlim(-5, t[-1]) ax.set_ylim(-24, 25) ax.legend(loc='upper right', fontsize=9, frameon=False) ax.grid(alpha=.17) fig.text(.26, .12, r'Same slope for both signs; zero intercept is assumed.', ha='center', fontsize=10) fig.text(.76, .12, r'$I=2\times10^{-6}$ kg m$^2$, $\gamma=0.020$ s$^{-1}$', ha='center', fontsize=10) fig.text(.5, .035, r'Period alone gives $0.92\kappa$; $\kappa=I(4\pi^2+\delta^2)/T_d^2$ recovers the input.', ha='center', fontsize=11) fig.savefig(Path(__file__).with_suffix('.png').name, dpi=110, facecolor='white', transparent=False) plt.close(fig) print(f'T0={period0:.9f} s; Td={periodd:.9f} s; delta={delta:.9f}; naive={inertia*omegad**2:.12g}; corrected={inertia*(omegad**2+gamma**2):.12g}') if __name__ == '__main__': main()