codex/einstein-de-haas-calibrated-torsion-response.py

einstein-de-haas-calibrated-torsion-response.py
"""Original calibrated forced-torsion model; no experimental data.

Python 3.14, numpy 2.3, matplotlib 3.10. Run from _media.
Independent numerical time-domain verification is documented in the task report.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    inertia = 2.00e-10
    stiffness = 8.00e-10
    damping = 8.00e-11
    gamma = -1.76e11
    moment = 1.76e-4
    omega0 = np.sqrt(stiffness / inertia)
    quality = inertia * omega0 / damping
    reference_torque = omega0 * moment / abs(gamma)
    omega = np.linspace(0, 6, 1401)
    mechanical = 1 / (stiffness - inertia * omega**2 + 1j*damping*omega)
    calibration = reference_torque * mechanical
    edh = (-1j*omega*moment/gamma) * mechanical
    phase_cal = np.angle(calibration, deg=True)
    phase_edh = 90 + phase_cal
    phase_edh[0] = np.nan  # zero response has no phase
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, (amplitude_ax, phase_ax) = plt.subplots(2, 1, figsize=(10, 7.3),
                                               sharex=True, facecolor="white")
    fig.subplots_adjust(top=.81, bottom=.12, left=.105, right=.96, hspace=.18)
    for ax in (amplitude_ax, phase_ax):
        ax.axvline(omega0, color="#777777", lw=1, ls=":")
        ax.grid(alpha=.20)
        ax.spines[["top", "right"]].set_visible(False)
        ax.set_xlim(0, 6)
    amplitude_ax.plot(omega, 1e6*abs(edh), color="#2468a0", lw=2.4,
                      label="Magnetic moment amplitude fixed: reaction torque ∝ ω")
    amplitude_ax.plot(omega, 1e6*abs(calibration), color="#c57b19", lw=2,
                      ls="--", label="Reference torque amplitude fixed")
    amplitude_ax.scatter([omega0], [12.5], s=48, color="#2468a0", zorder=4)
    amplitude_ax.annotate("12.5 μrad at ω₀ = 2 rad/s", (2, 12.5), (2.8, 10.5),
                          arrowprops={"arrowstyle": "->", "color": "#4b5158"})
    amplitude_ax.set(ylabel="Twist amplitude (μrad)", ylim=(0, 15.7))
    amplitude_ax.legend(frameon=False, loc="upper right", fontsize=10)
    phase_ax.plot(omega, phase_edh, color="#2468a0", lw=2.4)
    phase_ax.plot(omega, phase_cal, color="#c57b19", lw=2, ls="--")
    phase_ax.scatter([omega0], [0], color="#2468a0", zorder=4)
    phase_ax.scatter([omega0], [-90], color="#c57b19", zorder=4)
    phase_ax.annotate("in phase with magnetic moment", (2, 0), (2.8, 32),
                     arrowprops={"arrowstyle": "->", "color": "#2468a0"}, color="#2468a0")
    phase_ax.annotate("90° behind reference torque", (2, -90), (2.8, -65),
                     arrowprops={"arrowstyle": "->", "color": "#c57b19"}, color="#9b5e11")
    phase_ax.set(xlabel="Drive angular frequency ω (rad/s)",
                 ylabel="Twist phase (degrees)\nrelative to each input cosine",
                 ylim=(-190, 105), yticks=[-180, -90, 0, 90])
    fig.suptitle("Calibrated forced torsion: one oscillator, two different drives",
                 y=.975, weight="bold", fontsize=15)
    fig.text(.5, .917, "Stipulated model: I = 2 × 10⁻¹⁰ kg m²; κ = 8 × 10⁻¹⁰ N m/rad; "
             "b = 8 × 10⁻¹¹ N m s/rad", ha="center", fontsize=10.5)
    fig.text(.5, .875, "m₁ = 1.76 × 10⁻⁴ A m²; γ = −1.76 × 10¹¹ s⁻¹ T⁻¹; "
             f"Q = {quality:g}; reference torque = 2 × 10⁻¹⁵ N m", ha="center", fontsize=10.5)
    fig.text(.5, .035, "Calculated responses, not measurements. "
             "Mechanical transfer function: Hτ(ω) = 1 / (κ − Iω² + i bω)",
             ha="center", fontsize=10.5, color="#4b5158")
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()