codex/diffraction-grating-orders.py

diffraction-grating-orders.py
"""Normal-incidence grating directions; output a white PNG in the working directory.

Uses the repository's numpy/matplotlib dependencies; tested with Python 3.14.
"""

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt


def main():
    wavelength_nm, spacing_nm = 600.0, 2000.0
    orders = np.arange(-3, 4)
    sines = orders * wavelength_nm / spacing_nm
    angles = np.degrees(np.arcsin(sines))
    assert np.all(np.abs(sines) <= 1)
    assert 4 * wavelength_nm / spacing_nm > 1
    np.testing.assert_allclose(np.sin(np.radians(angles)), sines, atol=1e-15)

    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white", "axes.facecolor": "white"})
    fig, (fan, color) = plt.subplots(1, 2, figsize=(12, 6.2), gridspec_kw={"width_ratios": [1.15, 1]})
    fig.subplots_adjust(left=0.04, right=0.97, top=0.81, bottom=0.21, wspace=0.20)
    fig.suptitle("Normal-incidence transmission grating", fontsize=19, y=0.97)
    fig.text(0.5, 0.90, "600 nm light  •  spacing d = 2 μm  •  sin θ = 0.3 m", ha="center", fontsize=15)

    fan.axvline(0, color="#777777", lw=5, ymin=0.12, ymax=0.88)
    fan.axhline(0, color="#888888", lw=1, ls="--")
    fan.annotate("", (0, 0), (-0.75, 0), arrowprops={"arrowstyle": "->", "lw": 2.5, "color": "#405b75"})
    fan.text(-0.80, -0.23, "Incident\nplane wave", ha="center", va="top", fontsize=11)
    fan.text(0, -1.19, "Grating", ha="center", color="#555555")
    for m, sine, theta in zip(orders, sines, angles):
        direction = np.array([np.sqrt(1 - sine**2), sine])
        end = 1.03 * direction
        fan.annotate("", end, (0, 0), arrowprops={"arrowstyle": "->", "lw": 2.0, "color": "#c76921"})
        label = 1.17 * direction
        fan.text(*label, f"m = {m:+d}\n{theta:+.3f}°" if m else "m = 0\n0°", ha="left", va="center", fontsize=11)
    fan.text(1.49, -0.23, "Normal", fontsize=11, color="#666666")
    fan.set(xlim=(-1.06, 1.98), ylim=(-1.28, 1.28), aspect="equal")
    fan.axis("off")
    fan.set_title("Seven allowed directions", fontsize=15, pad=14)

    wavelengths = np.array([450, 500, 550, 600, 650])
    color_angles = np.degrees(np.arcsin(wavelengths / spacing_nm))
    colors = ["#5046ad", "#007ca5", "#32904a", "#c76921", "#b02b35"]
    for nm, theta, shade in zip(wavelengths, color_angles, colors):
        color.plot([theta, theta], [0.25, 0.77], color=shade, lw=3)
        color.text(theta, 0.81, f"{nm}\nnm", ha="center", color=shade, fontsize=11)
        color.text(theta, 0.14, f"{theta:.2f}°", ha="center", color=shade, fontsize=11)
    color.set(xlim=(12, 20.1), ylim=(0, 1.15), yticks=[], xlabel="First-order angle θ (degrees)")
    color.set_title("Longer wavelengths: larger +1 angle", fontsize=14, pad=14)
    color.set_xticks([13, 15, 17, 19])
    for side in ["left", "right", "top"]:
        color.spines[side].set_visible(False)
    fig.text(0.5, 0.11, "m = ±4 would require sin θ = ±1.2: no propagating direction.", ha="center", fontsize=14)
    fig.text(0.5, 0.055, "Ray arrows mark order directions, not brightness or finite peak width.", ha="center", fontsize=12, color="#555555")
    fig.savefig("diffraction-grating-orders.png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)
    print("Orders:", list(zip(orders.tolist(), sines.tolist(), angles.tolist())))
    print("First-order colour angles:", list(zip(wavelengths.tolist(), color_angles.tolist())))


if __name__ == "__main__":
    main()