codex/finite-grating-interference.py

finite-grating-interference.py
"""Ideal coherent grating array and slit envelope; PNG output in working directory.

Uses existing numpy/matplotlib dependencies; tested with Python 3.14.
"""

import numpy as np
import matplotlib

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


RATIO = 0.3  # wavelength / spacing: 600 nm / 2 micrometres


def array_factor(sine, count):
    # Reduce modulo one to evaluate removable singularities stably at every order.
    cycles = (np.asarray(sine) / RATIO + 0.5) % 1.0 - 0.5
    return (np.sinc(count * cycles) / np.sinc(cycles)) ** 2


def main():
    counts = (10, 40)
    for count in counts:
        width = RATIO / count
        np.testing.assert_allclose(array_factor(0.3, count), 1.0, atol=1e-14)
        np.testing.assert_allclose(array_factor(np.array([0.3 - width, 0.3 + width]), count), 0.0, atol=1e-25)
        samples = np.array([-0.913, -0.601, 0.0, 0.277, 0.3, 0.318, 0.719])
        phases = 2 * np.pi * samples / RATIO
        direct = np.abs(np.exp(1j * phases[:, None] * np.arange(count)).mean(axis=1)) ** 2
        np.testing.assert_allclose(array_factor(samples, count), direct, atol=1e-14)
        zeros = 0.3 + np.array([-width, width])
        print(f"N={count}: first zeros sin(theta)={zeros.tolist()}, one-sided width={width:g}")
        print("  zero angles (degrees):", np.degrees(np.arcsin(zeros)).tolist())

    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white", "axes.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(12, 5.6))
    fig.subplots_adjust(left=0.075, right=0.98, bottom=0.24, top=0.77, wspace=0.27)
    fig.suptitle("Finite coherent arrays: sharp orders inside a broad slit envelope", fontsize=17, y=0.97)
    fig.text(0.5, 0.89, "Normal incidence  •  λ = 600 nm  •  d = 2 μm", ha="center", fontsize=14)
    s = np.linspace(-1, 1, 24001)
    slit_width_nm = 500.0
    envelope = np.sinc(slit_width_nm * s / 600.0) ** 2
    axes[0].plot(s, array_factor(s, 40) * envelope, color="#246ba0", lw=1.1, label="N = 40, slit width a = 0.5 μm")
    axes[0].plot(s, envelope, color="#777777", ls="--", lw=1.8, label="Single-slit envelope")
    axes[0].set(xlim=(-1, 1), ylim=(0, 1.4), xlabel="sin θ", ylabel="Intensity / central intensity", title="Slit width sets the broad envelope")
    axes[0].legend(fontsize=10, loc="upper center")
    axes[0].set_xticks([-0.9, -0.6, -0.3, 0, 0.3, 0.6, 0.9])
    axes[0].grid(alpha=0.18)

    zoom = np.linspace(0.255, 0.345, 12001)
    for count, shade in zip(counts, ["#c76921", "#246ba0"]):
        width = RATIO / count
        axes[1].plot(zoom, array_factor(zoom, count), color=shade, lw=2, label=f"N = {count}: Δsin θ = {width:g}")
        axes[1].scatter([0.3 - width, 0.3 + width], [0, 0], color=shade, s=35, zorder=5)
        axes[1].annotate("", (0.3 + width, -0.09 if count == 10 else -0.18), (0.3 - width, -0.09 if count == 10 else -0.18), arrowprops={"arrowstyle": "<->", "color": shade, "lw": 1.6})
    axes[1].axvline(0.3, color="#aaaaaa", ls=":", lw=1)
    axes[1].set(xlim=(0.255, 0.345), ylim=(-0.23, 1.4), xlabel="sin θ, near first order", ylabel="Array factor / peak value", title="Four times as many slits: ¼ the width")
    axes[1].set_yticks([0, 0.25, 0.5, 0.75, 1])
    axes[1].legend(fontsize=10, loc="upper center")
    axes[1].grid(alpha=0.18)
    for ax in axes:
        for side in ["top", "right"]:
            ax.spines[side].set_visible(False)
    fig.text(0.5, 0.13, "Dots: first array zeros. Arrows: full zero-to-zero widths 0.06 and 0.015.", ha="center", fontsize=12)
    fig.text(0.5, 0.065, "Ideal uniform coherent illumination; right panel omits the slit envelope and normalizes both peaks to 1.", ha="center", fontsize=11, color="#555555")
    fig.savefig("finite-grating-interference.png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()