codex/bessel-lens-position-focal-length-sensitivity.py

bessel-lens-position-focal-length-sensitivity.py
"""Draw Bessel position and inference sensitivity, using stipulated ideal models.

Python 3.14, matplotlib 3.10.7, numpy 2.3.5. Run from _media; output uses script 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, (ax, bx) = plt.subplots(1, 2, figsize=(10.2, 4.6), facecolor="white")
    f = 10.0
    L = np.linspace(4 * f, 80, 600)
    d = np.sqrt(L * (L - 4 * f))
    ax.axvspan(25, 40, color="#eee9e5")
    ax.plot(L, (L - d) / 2, color="#2871a3", lw=2.2, label="$x_-$ (enlarged)")
    ax.plot(L, (L + d) / 2, color="#d46b2b", lw=2.2, label="$x_+$ (reduced)")
    ax.plot(L, L / 2, color="#999999", ls=":", label="Midpoint L/2")
    ax.scatter([40], [20], color="#333333", s=35, zorder=5)
    ax.annotate("One setting\nL = 4f", (40, 20), (29, 34), fontsize=10,
                arrowprops={"arrowstyle": "->", "color": "#555555"})
    ax.text(32.5, 7, "No real\nsetting", ha="center", color="#777777")
    ax.scatter([45, 45], [15, 30], color=["#2871a3", "#d46b2b"], s=38, zorder=5)
    ax.annotate("", (45, 15), (45, 30), arrowprops={"arrowstyle": "<->", "color": "#444444"})
    ax.text(46.5, 22, "d = 15 cm", fontsize=10)
    ax.set(xlim=(25, 80), ylim=(0, 73), xlabel="Object-screen separation L (cm)",
           ylabel="Lens position x (cm)", title="Fixed focal length f = 10 cm")
    ax.legend(loc="upper left", frameon=False, fontsize=9)
    ax.grid(alpha=0.18)

    L_fixed = 45.0
    measured_d = np.linspace(0, L_fixed, 500)
    inferred_f = (L_fixed**2 - measured_d**2) / (4 * L_fixed)
    bx.plot(measured_d, inferred_f, color="#27773f", lw=2.2)
    bx.axhline(L_fixed / 4, color="#aaaaaa", ls=":")
    bx.text(29, 11.65, "Upper bound L/4", fontsize=9, color="#777777")
    bx.scatter([15], [10], color="#27773f", s=42, zorder=5)
    tangent_d = np.array([3.0, 29.0])
    bx.plot(tangent_d, 10 - (tangent_d - 15) / 6, color="#7152a1", ls="--", lw=1.6)
    bx.annotate("d = 15 cm → f = 10 cm", (15, 10), (19, 8.4), fontsize=10,
                arrowprops={"arrowstyle": "->", "color": "#555555"})
    bx.text(4, 3.6, "$\\partial f/\\partial d = -d/(2L)$\n\nAt d = 15 cm: slope = −1/6", fontsize=11,
            bbox={"facecolor": "white", "edgecolor": "#dddddd", "pad": 7})
    bx.set(xlim=(0, 45), ylim=(0, 13.4), xlabel="Lens displacement d (cm)",
           ylabel="Inferred focal length f (cm)", title="Fixed separation L = 45 cm")
    bx.grid(alpha=0.18)
    fig.suptitle("Bessel method: position sensitivity and focal-length inference", fontsize=15)
    fig.text(0.5, 0.025, "Original thin-lens predictions. Coloured dots mark stipulated values, not observations.",
             ha="center", fontsize=10, color="#555555")
    fig.subplots_adjust(left=0.075, right=0.98, top=0.83, bottom=0.18, wspace=0.30)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=130, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()