codex/electron-velocity-selector.py

electron-velocity-selector.py
"""Draw an ideal electron velocity selector; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media: MPLBACKEND=Agg python ../electron-velocity-selector.py
The off-speed tracks solve the crossed-field equations exactly in scaled units.
"""
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np


def flight_time(x, u):
    """Invert x=t+(u-1)sin(t), with m=e=B=E=1 and u=v_initial/v_selected."""
    lo, hi = 0.0, 5.0
    for _ in range(60):
        mid = (lo + hi) / 2
        if mid + (u - 1) * np.sin(mid) < x:
            lo = mid
        else:
            hi = mid
    return (lo + hi) / 2


def main():
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig = plt.figure(figsize=(10, 5), dpi=120, facecolor="white")
    ax = fig.add_axes((.04, .22, .65, .63))
    forces = fig.add_axes((.73, .28, .25, .49))
    fig.suptitle("Ideal electron velocity selector", fontsize=18, y=.96)
    ax.add_patch(Rectangle((0, -.72), 2, 1.44, color="#eaf3fa", zorder=0))
    for y, sign in [(.76, "−"), (-.76, "+")]:
        ax.plot([0, 2], [y, y], lw=6, color="#536877", solid_capstyle="butt")
        for x in np.linspace(.15, 1.85, 7):
            ax.text(x, y + (.13 if y > 0 else -.15), sign, ha="center", va="center", fontsize=16)
    for x in np.linspace(.15, 1.85, 6):
        for y in [-.52, .52]:
            ax.plot(x, y, ".", color="#8498a7", ms=5)
    ax.text(.1, 1.11, r"$\mathbf{B}_s$ out of page (dots)", fontsize=12)
    ax.annotate("", xy=(1.48, .53), xytext=(1.48, .12), arrowprops={"arrowstyle": "->", "lw": 2, "color": "#495965"})
    ax.text(1.55, .4, r"$\mathbf{E}$", fontsize=15)
    tracks = [(.8, "#b45b1a", r"$0.8v_0$: bends down"), (1., "#087c73", r"$v_0=E/B_s$: straight"), (1.2, "#7d4ab0", r"$1.2v_0$: bends up")]
    for u, color, label in tracks:
        t = np.linspace(0, flight_time(2.15, u), 400)
        # The uniform fields end at x=2. Drift freely to the aperture.
        te = flight_time(2, u)
        tf = np.minimum(t, te)
        x = tf + (u - 1) * np.sin(tf)
        y = (u - 1) * (1 - np.cos(tf))
        after = np.maximum(t - te, 0)
        x += after * (1 + (u - 1) * np.cos(te))
        y += after * (u - 1) * np.sin(te)
        keep = x <= 2.15
        ax.plot(np.r_[-.55, x[keep]], np.r_[0, y[keep]], color=color, lw=2.6)
        label_y = -.14 if u == 1 else y[keep][-1]
        ax.text(2.27, label_y, label, color=color, fontsize=11, va="center")
    for low, high in [(-.72, -.055), (.055, .72)]:
        ax.plot([2.15, 2.15], [low, high], lw=4, color="#344450")
    ax.plot([2.15, 2.65], [0, 0], color="#087c73", lw=2.6)
    ax.text(2.15, -.96, "exit slit", ha="center", fontsize=11)
    ax.text(-.52, -.16, r"$e^-$", fontsize=15)
    ax.annotate("", xy=(-.03, .1), xytext=(-.5, .1), arrowprops={"arrowstyle": "->", "color": "#344450", "lw": 1.6})
    ax.text(-.53, .2, "beam", fontsize=11)
    ax.set(xlim=(-.6, 3.65), ylim=(-1.05, 1.3))
    ax.axis("off")
    forces.scatter([0], [0], s=130, color="#087c73")
    forces.text(.09, 0, r"$q=-e$", va="center")
    forces.annotate("", xy=(0, .83), xytext=(0, .08), arrowprops={"arrowstyle": "->", "lw": 2.5, "color": "#7d4ab0"})
    forces.annotate("", xy=(0, -.83), xytext=(0, -.08), arrowprops={"arrowstyle": "->", "lw": 2.5, "color": "#b45b1a"})
    forces.text(.1, .57, r"$F_B=ev_0B_s$", color="#7d4ab0", va="center")
    forces.text(.1, -.57, r"$F_E=eE$", color="#b45b1a", va="center")
    forces.set(xlim=(-.3, 1.4), ylim=(-1.25, 1.15))
    forces.text(-.2, 1.0, "Selected electron", fontsize=13)
    forces.text(-.2, -1.15, "Equal, opposite forces", fontsize=11)
    forces.axis("off")
    fig.text(.05, .12, r"$\mathbf{v}\perp\mathbf{E}\perp\mathbf{B}_s$; the slit accepts a finite band of speeds.", fontsize=12)
    fig.text(.05, .055, "Original teaching model; scaled geometry, not Thomson's 1897 tube.", fontsize=11, color="#495965")
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()