codex/magnetic-dipole-angular-energy-torque.py

magnetic-dipole-angular-energy-torque.py
"""Draw an imposed field, signed torque, and angular orientation energy.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
Run from _media; the PNG is written to the current working directory.
"""

from pathlib import Path

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


def main():
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig = plt.figure(figsize=(10.2, 4.8), facecolor="white")
    grid = fig.add_gridspec(1, 2, width_ratios=[1, 1.75], wspace=0.31)
    geometry, curves = fig.add_subplot(grid[0]), fig.add_subplot(grid[1])
    blue, orange = "#1965a6", "#a84c16"
    theta = np.pi / 3
    geometry.set_aspect("equal")
    geometry.set_xlim(-0.65, 1.65)
    geometry.set_ylim(-0.82, 1.55)
    geometry.axis("off")
    for end, color in [((1.45, 0), blue), ((1.20 * np.cos(theta), 1.20 * np.sin(theta)), orange)]:
        geometry.add_patch(FancyArrowPatch((0, 0), end, arrowstyle="-|>",
                                          mutation_scale=22, lw=2.6, color=color))
    geometry.text(1.43, -0.22, r"$\mathbf{B}_{\rm ext}$", color=blue, ha="center", fontsize=16)
    geometry.text(0.67, 1.06, r"$\mathbf{m}$", color=orange, fontsize=18)
    geometry.add_patch(Arc((0, 0), 0.75, 0.75, theta1=0, theta2=60, lw=1.4))
    geometry.text(0.40, 0.25, r"$\theta>0$", fontsize=14)
    angles = np.linspace(np.deg2rad(135), np.deg2rad(25), 80)
    geometry.plot(0.78 * np.cos(angles), 0.78 * np.sin(angles), color="#494949", lw=1.6)
    geometry.add_patch(FancyArrowPatch((0.78 * np.cos(angles[-3]), 0.78 * np.sin(angles[-3])),
                                      (0.78 * np.cos(angles[-1]), 0.78 * np.sin(angles[-1])),
                                      arrowstyle="-|>", mutation_scale=16, color="#494949"))
    geometry.text(0.48, 1.42, "Torque turns clockwise", ha="center", fontsize=12)
    geometry.text(0.45, -0.50, r"$\tau_z=-mB\sin\theta<0$", ha="center", fontsize=13)
    geometry.text(0.45, -0.74, "+z points out of the page", ha="center", fontsize=10)
    angle = np.linspace(0, 2 * np.pi, 801)
    curves.plot(angle, -np.cos(angle), color=blue, lw=2.5, label=r"$U/(mB)=-\cos\theta$")
    curves.plot(angle, -np.sin(angle), color=orange, lw=2.5, label=r"$\tau_z/(mB)=-\sin\theta$")
    curves.axhline(0, color="#666666", lw=0.8)
    curves.set_xlim(0, 2 * np.pi)
    curves.set_ylim(-1.43, 1.65)
    curves.set_xticks([0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi],
                     ["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$", r"$2\pi$"])
    curves.set_yticks([-1, 0, 1])
    curves.set_xlabel(r"Angle $\theta$ from the external field (radians)")
    curves.set_ylabel("Normalized energy / signed torque")
    curves.grid(alpha=0.18)
    curves.spines[["top", "right"]].set_visible(False)
    curves.legend(loc="upper right", frameon=False, fontsize=11)
    curves.scatter([0, np.pi, 2 * np.pi], [-1, 1, -1], color=blue, zorder=5, s=25)
    curves.annotate("stable", xy=(0, -1), xytext=(0.43, -1.29), fontsize=11,
                    arrowprops={"arrowstyle": "-", "color": "#444444"})
    curves.annotate("unstable", xy=(np.pi, 1), xytext=(2.02, 1.29), fontsize=11,
                    arrowprops={"arrowstyle": "-", "color": "#444444"})
    fig.suptitle("Magnetic dipole: orientation energy and restoring torque", fontsize=16, weight="bold")
    fig.text(0.5, 0.025, r"Fixed $m$ and $B$  •  $\tau_z=-dU/d\theta$  •  $mB$: joules for energy, newton metres for torque",
             ha="center", fontsize=11)
    fig.subplots_adjust(left=0.035, right=0.975, top=0.84, bottom=0.18)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=110,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()