codex/cavendish-torsion-balance-schematic.py

cavendish-torsion-balance-schematic.py
"""Original toy geometry, not historical apparatus; output PNG basename to CWD.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch
import numpy as np


def main():
    blue, orange, green = "#245f9b", "#b95d16", "#187b58"
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
    fig, (top, side) = plt.subplots(1, 2, figsize=(10.4, 5.6), dpi=120,
                                   gridspec_kw={"width_ratios": [1.65, 1]})
    fig.suptitle("Two near-pair forces turn the suspended rod", fontsize=16)
    fig.subplots_adjust(left=.03, right=.98, bottom=.18, top=.82, wspace=.16)
    top.set(xlim=(-1.8, 1.8), ylim=(-1.25, 1.25), aspect="equal")
    top.axis("off")
    top.set_title("Top view: zero twist", fontsize=12)
    L, r = 1.0, .65  # Drawing coordinates only; sphere radii are illustrative.
    test = np.array([[-L, 0.0], [L, 0.0]])
    source = np.array([[-L, -r], [L, r]])
    directions = (source - test) / r
    torques = test[:, 0] * directions[:, 1] - test[:, 1] * directions[:, 0]
    assert np.all(torques > 0)
    reversed_torques = test[:, 0] * (-directions[:, 1])
    assert np.all(reversed_torques < 0)
    top.plot([-L, L], [0, 0], color="#555555", lw=5, zorder=1)
    for (x, y), (sx, sy), direction in zip(test, source, directions):
        top.plot([x, sx], [y, sy], color="#aaaaaa", ls=":", lw=1.5)
        top.add_patch(Circle((sx, sy), .18, facecolor="#fff1e4",
                             edgecolor=orange, lw=2, zorder=2))
        top.text(sx, sy, "$M$", ha="center", va="center", color=orange)
        top.add_patch(Circle((x, y), .09, facecolor="#e7f1ff",
                             edgecolor=blue, lw=2, zorder=3))
        top.text(x + (-.17 if x < 0 else .17), .08, "$m$",
                 color=blue, ha="right" if x < 0 else "left")
        start = (x, direction[1] * .12)
        end = (x, direction[1] * .43)
        top.add_patch(FancyArrowPatch(start, end, arrowstyle="-|>",
                                     mutation_scale=17, color=blue, lw=2))
        top.text(x + .17, direction[1] * .32, "$F$", color=blue,
                 ha="left", va="center")
    top.text(-L, -1.00, "$(-L,-r)$", ha="center", color=orange)
    top.text(L, 1.00, "$(L,r)$", ha="center", color=orange)
    top.annotate("", (-1.42, -r), (-1.42, 0),
                 arrowprops={"arrowstyle": "<->", "color": "#666666"})
    top.text(-1.56, -r / 2, "$r$", ha="center", va="center")
    top.annotate("", (0, -.99), (L, -.99),
                 arrowprops={"arrowstyle": "<->", "color": "#666666"})
    top.text(.5 * L, -1.14, "$L$ = half the rod length", ha="center", fontsize=10)
    top.plot(0, 0, "o", color="#222222", ms=6)
    top.text(0, -.16, "fiber axis", ha="center", fontsize=10)
    # Positive polar angle: counterclockwise, viewed along the vertical axis.
    angles = np.linspace(.05, 1.6 * np.pi, 100)
    radius = .34
    x, y = radius * np.cos(angles), radius * np.sin(angles)
    top.plot(x, y, color=green, lw=2)
    top.add_patch(FancyArrowPatch((x[-3], y[-3]), (x[-1], y[-1]),
                                 arrowstyle="-|>", mutation_scale=14,
                                 color=green, lw=2))
    top.text(0, .53, r"$\tau_g=+2LF$", ha="center", color=green, fontsize=13)
    side.set(xlim=(-1.3, 1.3), ylim=(-.45, 2.0))
    side.axis("off")
    side.set_title("Side view: elastic suspension", fontsize=12)
    side.plot([-.4, .4], [1.68, 1.68], color="#555555", lw=7)
    side.plot([0, 0], [.35, 1.68], color="#333333", lw=2)
    side.text(.12, 1.05, "thin vertical\nfiber", va="center")
    side.plot([-1, 1], [.35, .35], color="#555555", lw=4)
    side.plot([-1, 1], [.35, .35], "o", ms=12, color=blue)
    side.text(0, .08, "rod + two test masses", ha="center", fontsize=10)
    side.text(0, -.20, r"$\tau_{\rm fiber}=-\kappa\theta$", ha="center",
              fontsize=13, color=green)
    fig.text(.5, .085, "Left force down + right force up: both torques counterclockwise.",
             ha="center", fontsize=12)
    fig.text(.5, .035, "Toy model: opposite-source forces and rod mass omitted; not to scale.",
             ha="center", fontsize=10, color="#555555")
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"),
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()