codex/aperture-focal-ratio-field-view-cone-scaling.py

aperture-focal-ratio-field-view-cone-scaling.py
"""Original thin-lens cone geometry and analytical scalings; no measured data.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media; the output basename follows this generator's basename.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, (geometry, scaling) = plt.subplots(1, 2, figsize=(11, 5.3))
    fig.patch.set_facecolor("white")
    colors = ["#2364aa", "#c05a13"]
    f_mm, wavelength_um = 100.0, 0.55
    geometry.axhline(0, color="0.65", lw=1, ls="--")
    geometry.axvline(f_mm, color="0.5", lw=1, ls=":")
    for diameter, color in zip([25.0, 12.5], colors):
        number = f_mm / diameter
        x = np.array([0.0, f_mm])
        for sign in [-1, 1]:
            y = sign * diameter / 2 * (1 - x / f_mm)
            geometry.plot([-15, 0], [y[0], y[0]], color=color, lw=2)
            geometry.plot(x, y, color=color, lw=2)
            assert abs(y[-1]) < 1e-12
        geometry.plot([0, 0], [-diameter/2, diameter/2], color=color,
                      lw=4, alpha=0.65)
        geometry.plot([], [], color=color, lw=2,
                      label=f"D = {diameter:g} mm, f/{number:g}")
    geometry.scatter([f_mm], [0], color="black", s=25, zorder=4)
    geometry.annotate("common focus", (100, 0), (60, -25),
                      arrowprops={"arrowstyle": "->", "color": "0.3"})
    geometry.text(0, 22, "thin lens + stop", ha="center")
    geometry.annotate("", (0, -19), (100, -19),
                      arrowprops={"arrowstyle": "<->", "color": "0.3"})
    geometry.text(50, -16, "f = 100 mm", ha="center")
    geometry.set(xlim=(-18, 113), ylim=(-30, 30),
                 xlabel="axial position (mm)", ylabel="ray height (mm)",
                 title="Smaller opening, same focal length")
    geometry.set_aspect("equal", adjustable="box")
    geometry.legend(loc="upper left", bbox_to_anchor=(0, -0.21), frameon=False)
    numbers = np.linspace(4, 16, 301)
    flux_relative = (4 / numbers) ** 2
    radius_relative = numbers / 4
    scaling.plot(numbers, radius_relative, color="#7042a1", lw=2.5,
                 label="first dark-ring radius / radius at f/4")
    scaling.plot(numbers, flux_relative, color="#16806a", lw=2.5,
                 label="collected flux / flux at f/4")
    scaling.axhline(1, color="0.65", lw=1, ls=":")
    for n in [4, 8, 16]:
        scaling.scatter([n], [(4/n)**2], color="#16806a", zorder=4)
        scaling.scatter([n], [n/4], color="#7042a1", zorder=4)
    scaling.annotate("f/8: radius ×2", (8, 2), (9, 2.15), color="#7042a1")
    scaling.annotate("f/8: flux ×¼", (8, 0.25), (9, 0.55), color="#16806a")
    scaling.set(xlim=(3.6, 16.4), ylim=(0, 4.4), xticks=[4, 8, 12, 16],
                xlabel="f-number N = f / D", ylabel="dimensionless ratio",
                title="Analytical scaling at fixed f and λ")
    scaling.legend(loc="upper left", bbox_to_anchor=(-0.04, -0.15),
                   frameon=False, fontsize=9.5)
    assert np.isclose(1.22 * wavelength_um * 4, 2.684)
    assert np.isclose((12.5/25)**2, 0.25)
    assert np.isclose(np.tan(np.arctan(25/(2*f_mm))), 1/8)
    fig.suptitle("Aperture and focal ratio: cone, collection and diffraction", fontsize=16)
    fig.text(0.5, 0.015,
             "Ideal unobscured circular pupil; uniform incident beam; λ = 0.55 μm. Calculated, not measured.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.075, right=0.97, bottom=0.23, top=0.85, wspace=0.32)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=110,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()