codex/galvanometer-radial-field-coil-torque.py

galvanometer-radial-field-coil-torque.py
"""Draw a moving-coil force couple; Python 3.14, matplotlib 3.10.7, NumPy 2.3.5."""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch, Wedge
import numpy as np


def arrow(ax, start, end, color, width=2.0, style="-|>"):
    ax.add_patch(FancyArrowPatch(start, end, arrowstyle=style,
                                mutation_scale=15, linewidth=width, color=color))


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
    fig, (ax, notes) = plt.subplots(1, 2, figsize=(9.2, 4.9),
                                  gridspec_kw={"width_ratios": [1.5, 1]})
    blue, red, green = "#2166ac", "#b2182b", "#397343"
    ax.set_aspect("equal")
    ax.set_xlim(-2.35, 2.35)
    ax.set_ylim(-1.95, 2.05)
    ax.axis("off")
    for label, angles, color in [("N", (145, 215), "#f4ddd7"),
                                  ("S", (-35, 35), "#dce8f4")]:
        ax.add_patch(Wedge((0, 0), 1.9, *angles, width=0.63,
                           facecolor=color, edgecolor="#555555", linewidth=1.5))
        ax.text(-1.60 if label == "N" else 1.60, 0, label,
                ha="center", va="center", fontsize=18, weight="bold")
    ax.add_patch(Circle((0, 0), 0.70, facecolor="#ededed",
                        edgecolor="#777777", linewidth=1.4))
    ax.text(0, -0.30, "fixed iron\ncore", ha="center", va="center")
    ax.plot(0, 0, "k.", ms=7)
    alpha = np.deg2rad(20)
    radial = np.array([np.cos(alpha), np.sin(alpha)])
    tangent = np.array([-np.sin(alpha), np.cos(alpha)])
    # Left gap: N -> core; right gap: core -> S. Both local B vectors
    # point along +radial, though one is inward and the other outward.
    for degrees in [-24, 0, 24]:
        e = np.array([np.cos(np.deg2rad(degrees)), np.sin(np.deg2rad(degrees))])
        arrow(ax, -1.23 * e, -0.76 * e, blue, width=1.6)
        arrow(ax, 0.76 * e, 1.23 * e, blue, width=1.6)
    left, right = -1.06 * radial, 1.06 * radial
    ax.plot([left[0], right[0]], [left[1], right[1]], "--",
            color="#7b4b18", linewidth=2.1)
    for position, sign in [(left, -1), (right, 1)]:
        ax.add_patch(Circle(position, 0.095, facecolor="white",
                            edgecolor="#7b4b18", linewidth=2, zorder=5))
        ax.text(*position, "×" if sign == -1 else "•", ha="center", va="center",
                color="#7b4b18", fontsize=14, zorder=6)
        arrow(ax, position + sign * 0.13 * tangent,
              position + sign * 0.90 * tangent, red)
    ax.text(-1.0, -1.38, r"$F=N I\ell B$", color=red, ha="center")
    ax.text(1.04, 1.36, r"$F=N I\ell B$", color=red, ha="center")
    ax.text(-1.98, 1.34, "shaped poles", ha="left", fontsize=10)
    ax.text(0.55, -1.40, "coil-plane projection\n(dashed, not a wire\nthrough the core)",
            ha="center", fontsize=9.5, color="#7b4b18")
    ax.text(-0.30, 1.73, "positive rotation ↺", color=red, ha="center")
    ax.add_patch(FancyArrowPatch((1.46, 0.10), (0.74, 1.27),
                                connectionstyle="arc3,rad=0.24", arrowstyle="-|>",
                                color=red, linewidth=2, mutation_scale=15))
    ax.add_patch(FancyArrowPatch((-0.36, 0.43), (0.41, 0.33),
                                connectionstyle="arc3,rad=-0.6", arrowstyle="-|>",
                                color=green, linewidth=1.8, mutation_scale=13))
    notes.axis("off")
    notes.text(0.02, 0.94, "Top view along the rotation axis", fontsize=12, weight="bold")
    notes.text(0.02, 0.87, "⊙ current out of page\n⊗ current into page\nActive wire length ℓ: along the axis",
               linespacing=1.6, color="#7b4b18", va="top")
    notes.text(0.02, 0.55, "Local gap field B", color=blue, weight="bold")
    notes.text(0.02, 0.47, "Side forces form a couple", color=red, weight="bold")
    notes.text(0.02, 0.39, "Restoring spring torque ↻", color=green, weight="bold")
    notes.text(0.02, 0.29, r"$A=w\ell$" "\n" r"$\tau_{\rm m}=N I\ell B\,w=NIAB$"
               "\n" r"$\tau_{\rm s}=-k\theta$" "\n" r"$\theta=NIAB/k$ at rest", linespacing=1.6, va="top")
    fig.suptitle("Moving coil in a radial working-gap field", fontsize=15)
    fig.text(0.5, 0.025, "Schematic only • reverse I to reverse both forces and deflection • flux return path omitted",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.025, right=0.99, top=0.88, bottom=0.12, wspace=0.08)
    fig.savefig(Path(__file__).stem + ".png", dpi=130, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()