codex/angular-momentum-lever-arm.py

angular-momentum-lever-arm.py
"""Generate angular-momentum-lever-arm.png in the current working directory.

Use: make _media/angular-momentum-lever-arm.png
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Position is in metres; momentum is in kg m/s. Arrows use separate scales.
"""

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "savefig.facecolor": "white"})
    r = np.array([2.0, 0.0, 0.0])
    momenta = np.array([[0.0, 3.0, 0.0], [0.0, -3.0, 0.0], [3.0, 0.0, 0.0]])
    angular = np.cross(r, momenta)
    assert np.allclose(angular[:, 2], [6, -6, 0])
    shifted = np.cross(r - [1.0, 0.0, 0.0], momenta[0])
    assert np.allclose(shifted, [0, 0, 3])
    fig, axes = plt.subplots(1, 3, figsize=(10.5, 4.7), facecolor="white")
    fig.subplots_adjust(left=0.03, right=0.98, bottom=0.23, top=0.76, wspace=0.19)
    fig.suptitle("Angular momentum: transverse motion counts, radial motion does not",
                 fontsize=16, y=0.97)
    fig.text(0.5, 0.87, r"Same position: $\mathbf{r}=(2,0,0)$ m.  $L_z=xp_y-yp_x$; $+z$ points out of the page.",
             ha="center", fontsize=12)
    titles = ["Positive: out of the page", "Negative: into the page", "Radial: zero"]
    colors = ["#18775c", "#b04939", "#5b537c"]
    for i, (ax, p, angular_i, title, color) in enumerate(zip(axes, momenta, angular, titles, colors)):
        ax.set(xlim=(-0.35, 4.7), ylim=(-2.55, 2.55), aspect="equal")
        ax.axis("off")
        ax.set_title(title, fontsize=12, pad=12, color=color)
        ax.annotate("", (4.6, 0), (-0.25, 0), arrowprops={"arrowstyle": "->", "color": "#bbbbbb"})
        ax.annotate("", (0, 2.4), (0, -2.35), arrowprops={"arrowstyle": "->", "color": "#bbbbbb"})
        ax.text(4.5, -0.33, "x", color="#555555")
        ax.text(-0.25, 2.2, "y", color="#555555")
        if i < 2:
            ax.plot([2, 2], [-2.25, 2.25], ls="--", color="#d4b6a0", lw=1.4, zorder=0)
        ax.annotate("", (1.92, 0), (0, 0),
                    arrowprops={"arrowstyle": "-|>", "lw": 2.5, "color": "#2469a0"})
        ax.text(0.85, -0.38, r"$\mathbf{r}$", color="#2469a0", fontsize=14)
        ax.scatter([0, 2], [0, 0], s=[24, 60], color="#263341", zorder=5)
        ax.text(-0.25, -0.45, "O", color="#263341")
        ax.text(1.95, 0.3, "(2, 0) m", fontsize=10)
        tip = r[:2] + 0.68 * p[:2]
        ax.annotate("", tip, r[:2],
                    arrowprops={"arrowstyle": "-|>", "lw": 2.8, "color": "#c86c21"})
        if i < 2:
            ax.text(2.25, 0.9 if i == 0 else -1.25,
                    r"$\mathbf{p}$" + ("\n+3 y" if i == 0 else "\n−3 y"),
                    color="#a45213", fontsize=12)
            level = -2.15 if i == 0 else 2.05
            ax.annotate("", (2, level), (0, level),
                        arrowprops={"arrowstyle": "<->", "color": "#777777"})
            ax.text(1, level + (-0.35 if i == 0 else 0.22), r"$\ell=2$ m", ha="center", fontsize=11)
        else:
            ax.text(3.0, -0.65, r"$\mathbf{p}=3\,\hat{\mathbf{x}}$", color="#a45213", fontsize=12)
            ax.text(2.5, -1.8, r"$\mathbf{r}\parallel\mathbf{p}$; $\ell=0$", ha="center", fontsize=12)
        ax.text(0.5, -0.15, rf"$L_z={angular_i[2]:+g}$ kg m$^2$/s" if i < 2 else r"$L_z=0$ kg m$^2$/s",
                transform=ax.transAxes, ha="center", fontsize=14, color=color)
    fig.text(0.5, 0.045, "Blue: position. Orange: momentum (drawn to a separate scale). Dashed lines: direction of momentum.",
             ha="center", fontsize=10)
    fig.savefig("angular-momentum-lever-arm.png", dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()