codex/thin-lens-real-image.py

thin-lens-real-image.py
"""Generate thin-lens-real-image.png in CWD; tested with Python 3.14.4.

Uses the repository's numpy and matplotlib dependencies. Exact coordinates in
an ideal paraxial thin-lens model; vertical scale enlarged for visibility.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np


def direction_arrow(ax, x0, x1, height, color):
    """Add a rightward arrow on a physically transmitted ray segment."""
    assert x1 > x0
    ax.annotate("", xy=(x1, height(x1)), xytext=(x0, height(x0)),
                arrowprops={"arrowstyle": "->", "lw": 2.0, "color": color},
                zorder=6)


def main():
    focal, distance, object_height = 1.0, 3.0, 0.1
    image = focal * distance / (distance - focal)
    magnification = -image / distance
    image_height = magnification * object_height
    parallel = lambda x: object_height * (1 - x / focal)
    centre = lambda x: -object_height * x / distance
    assert np.allclose([image, magnification, image_height], [1.5, -0.5, -0.05],
                       atol=1e-14, rtol=0)
    assert np.isclose(parallel(focal), 0)
    assert np.isclose(centre(-distance), object_height)
    assert np.isclose(parallel(image), image_height)
    assert np.isclose(centre(image), image_height)
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, ax = plt.subplots(figsize=(10, 5.2), dpi=110)
    fig.subplots_adjust(left=.10, right=.98, top=.80, bottom=.23)
    ax.axhline(0, color="0.4", lw=1.1)
    ax.add_patch(Ellipse((0, 0), .14, .35, facecolor="#e5f1fa",
                         edgecolor="#6d90a8", lw=1.4, zorder=1))
    ax.axvline(0, color="#6d90a8", lw=.8, linestyle=":")
    blue, orange = "#1768ac", "#cb6418"
    incoming = np.linspace(-distance, 0, 100)
    outgoing = np.linspace(0, 2.1, 200)
    ax.plot(incoming, np.full_like(incoming, object_height), color=blue, lw=2)
    ax.plot(incoming, centre(incoming), color=orange, lw=2)
    ax.plot(outgoing, parallel(outgoing), color=blue, lw=2,
            label="Parallel in; through rear focus out")
    ax.plot(outgoing, centre(outgoing), color=orange, lw=2,
            label="Through centre; undeviated")
    direction_arrow(ax, -.72 * distance, -.46 * distance,
                    lambda x: object_height, blue)
    direction_arrow(ax, -.72 * distance, -.46 * distance, centre, orange)
    direction_arrow(ax, .42, .68, parallel, blue)
    direction_arrow(ax, .42, .68, centre, orange)
    
    ax.annotate("", xy=(-distance, object_height), xytext=(-distance, 0),
                arrowprops={"arrowstyle": "-|>", "color": "#237a48", "lw": 2.6},
                zorder=7)
    ax.annotate("", xy=(image, image_height), xytext=(image, 0),
                arrowprops={"arrowstyle": "-|>", "color": "#943d7c", "lw": 2.6},
                zorder=7)
    ax.scatter([-distance, image], [object_height, image_height],
               color=["#237a48", "#943d7c"], s=25, zorder=8)
    ax.scatter([-focal, focal], [0, 0], color="0.25", s=25, zorder=8)
    ax.annotate("Front focus (−1, 0)", xy=(-focal, 0), xytext=(0, -23),
                textcoords="offset points", ha="center", fontsize=10)
    ax.annotate("Rear focus (1, 0)", xy=(focal, 0), xytext=(0, 28),
                textcoords="offset points", ha="center", fontsize=10)
    ax.annotate("Object: (−3, 0.1)", xy=(-distance, object_height),
                xytext=(0, 18), textcoords="offset points",
                ha="center", color="#237a48", fontsize=11)
    ax.annotate("Real image: (1.5, −0.05)", xy=(image, image_height),
                xytext=(0, -55), textcoords="offset points",
                ha="center", color="#943d7c", fontsize=11)
    ax.text(0, .20, "Ideal lens at x = 0", ha="center", fontsize=10,
            color="#456b84")
    ax.set(xlim=(-3.45, 2.25), ylim=(-.17, .24), xticks=[-3, -2, -1, 0, 1, 1.5, 2],
           xlabel="Position x (m)", ylabel="Height y (m)")
    ax.grid(alpha=.15)
    fig.suptitle("Real image: outgoing rays meet beyond the lens", fontsize=16, y=.97)
    fig.text(.5, .875, r"$f=1$ m, $u=3$ m, $v=1.5$ m, $m=-0.5$;  $h_o=0.1$ m, $h_i=-0.05$ m", ha="center", fontsize=12)
    handles, labels = ax.get_legend_handles_labels()
    fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(.5, .08),
               ncol=2, frameon=False, fontsize=10)
    fig.text(.5, .025, "Ideal paraxial model in air. Vertical scale enlarged; arrows on solid rays show travel to the right.",
             ha="center", fontsize=10)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()