codeximage-magnification-real-rays.py
image-magnification-real-rays.py
"""Original paraxial ray diagrams; all values are predictions, not data.
Run from _media with MPLCONFIGDIR=/tmp/codex-wiki-matplotlib.
Tested with Python 3.14.4, matplotlib 3.10.7 and numpy 2.3.5.
The PNG is written to the current directory with this script's basename.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def ray(ax, xs, ys, color):
ax.plot(xs, ys, color=color, lw=2)
for k in range(len(xs) - 1):
start = np.array([xs[k], ys[k]])
step = np.array([xs[k + 1], ys[k + 1]]) - start
ax.annotate("", xy=start + 0.68 * step, xytext=start + 0.52 * step,
arrowprops={"arrowstyle": "->", "color": color, "lw": 2})
def diagram(ax, u, letter):
f, h = 10.0, 0.5 # centimetres
v = f * u / (u - f)
image_h = -v * h / u
# Check the construction using the independent paraxial lens kick.
# Arbitrary incident rays arriving at different lens heights must agree.
for intercept in np.linspace(-0.6, 0.6, 13):
slope_in = (intercept - h) / u
slope_out = slope_in - intercept / f
assert abs(intercept + v * slope_out - image_h) < 1e-12
ax.axhline(0, color="#777777", lw=1)
ax.plot([0, 0], [-1.25, 0.8], color="#25475c", lw=3)
ax.plot([v, v], [-1.25, 0.8], color="#a3a3a3", lw=5, alpha=0.6)
ax.text(0, 0.86, "thin lens", ha="center", fontsize=10)
ax.text(v, 0.86, "screen", ha="center", fontsize=10)
ax.scatter([-f, f], [0, 0], color="#222222", s=20, zorder=5)
for x, label in [(-f, "front F"), (f, "rear F")]:
ax.text(x, -0.17 if x < 0 else 0.08, label, ha="center", fontsize=9)
end = v # An opaque screen intercepts the bundle here.
ray(ax, [-u, 0, end], [h, h, h * (1 - end / f)], "#006ca8")
ray(ax, [-u, 0, end], [h, 0, -h * end / u], "#c95a16")
for x, height, color in [(-u, h, "#813c98"), (v, image_h, "#19764b")]:
ax.annotate("", xy=(x, height), xytext=(x, 0),
arrowprops={"arrowstyle": "-|>", "color": color,
"lw": 3, "mutation_scale": 17})
ax.text(-u, h + 0.11, "object: +0.50 cm", ha="center", color="#813c98",
fontsize=10)
ax.text(v, image_h - 0.28, f"image: {image_h:+.2f} cm", ha="center",
color="#19764b", fontsize=10)
m_text = "−1/2" if u == 30 else "−2"
ax.set_title(f"{letter} u = {u:g} cm, v = {v:g} cm, m = {m_text}",
loc="left", fontsize=12, pad=12)
ax.set_xlim(-36, 38)
ax.set_ylim(-1.48, 1.02)
ax.set_xticks([-30, -20, -10, 0, 10, 20, 30])
ax.set_yticks([-1, -0.5, 0, 0.5])
ax.set_ylabel("height (cm)", fontsize=10)
ax.spines[["top", "right"]].set_visible(False)
ax.tick_params(labelsize=9)
def main():
plt.rcParams.update({"font.family": "DejaVu Sans", "figure.facecolor": "white",
"axes.facecolor": "white"})
fig, axes = plt.subplots(2, 1, figsize=(9, 6.6))
for ax, u, letter in zip(axes, [30.0, 15.0], ["A", "B"]):
diagram(ax, u, letter)
axes[-1].set_xlabel("x (cm); light travels to the right →", fontsize=10)
fig.suptitle("Real images: rays meet on the screen", fontsize=15, y=0.98)
fig.text(0.5, 0.018,
"Ideal thin lens, f = 10 cm • same scales in both panels • vertical scale enlarged • no measured data",
ha="center", fontsize=9)
fig.subplots_adjust(left=0.09, right=0.97, bottom=0.11, top=0.89, hspace=0.5)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120,
facecolor="white", transparent=False)
plt.close(fig)
print("Real-image ray geometry: 26 arbitrary-height rays checked.")
if __name__ == "__main__":
main()