codexthin-lens-virtual-image.py
thin-lens-virtual-image.py
"""Generate thin-lens-virtual-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, 0.5, 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.0, 2.0, 0.2],
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=.32)
ax.axhline(0, color="0.4", lw=1.1)
ax.add_patch(Ellipse((0, 0), .075, .54, 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, 1.25, 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, .55, .78, parallel, blue)
direction_arrow(ax, .55, .78, centre, orange)
if image < 0:
backwards = np.linspace(image, 0, 100)
ax.plot(backwards, parallel(backwards), "--", color=blue, lw=1.5, zorder=1.5,
label="Dashed: backward extensions only")
ax.plot(backwards, centre(backwards), "--", color=orange, lw=1.5, zorder=1.5)
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, -23),
textcoords="offset points", ha="center", fontsize=10)
ax.annotate("Object: (−0.5, 0.1)", xy=(-distance, object_height),
xytext=(0, 18), textcoords="offset points",
ha="center", color="#237a48", fontsize=11)
ax.annotate("Virtual image: (−1, 0.2)", xy=(image, image_height),
xytext=(-30, 18), textcoords="offset points",
ha="center", color="#943d7c", fontsize=11)
ax.text(0, .29, "Ideal lens at x = 0", ha="center", fontsize=10,
color="#456b84")
ax.set(xlim=(-1.4, 1.4), ylim=(-.30, .35), xticks=[-1, -.5, 0, .5, 1],
xlabel="Position x (m)", ylabel="Height y (m)")
ax.grid(alpha=.15)
fig.suptitle("Virtual image: only backward extensions meet", fontsize=16, y=.97)
fig.text(.5, .875, r"$f=1$ m, $u=0.5$ m, $v=-1$ m, $m=2$; $h_o=0.1$ m, $h_i=0.2$ 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=1, 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()