codexoptical-refraction-rays.py
optical-refraction-rays.py
"""Draw optical-refraction-rays.png in cwd; verify angles and ray directions.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
From _media: MPLBACKEND=Agg MPLCONFIGDIR=/tmp python3 ../optical-refraction-rays.py
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
BLUE, GREEN, ORANGE = "#1769aa", "#23834c", "#d26a15"
def ray(ax, start, end, color):
ax.plot([start[0], end[0]], [start[1], end[1]], color=color, lw=3)
start, end = np.asarray(start), np.asarray(end)
ax.annotate("", xy=start + 0.68 * (end - start), xytext=start + 0.53 * (end - start),
arrowprops={"arrowstyle": "->", "lw": 2.8, "color": color})
def base(ax, upper, lower):
ax.set(xlim=(-1.08, 1.08), ylim=(-1.08, 1.08), aspect="equal")
ax.set_facecolor("white")
ax.axhline(0, color="#333333", lw=1.5)
ax.axvline(0, color="#777777", lw=1.4, ls="--")
ax.text(-1.03, 0.97, upper, va="top", fontsize=17)
ax.text(-1.03, -0.97, lower, va="bottom", fontsize=17)
ax.text(0.04, 0.98, "Normal", va="top", fontsize=14, color="#555555")
ax.axis("off")
def main():
theta1 = np.deg2rad(45)
theta2 = np.arcsin(np.sin(theta1) / 1.5)
critical = np.arcsin(1 / 1.5)
np.testing.assert_allclose(np.rad2deg(theta2), 28.1255057021, atol=1e-9, rtol=0)
np.testing.assert_allclose(np.rad2deg(critical), 41.8103148958, atol=1e-9, rtol=0)
np.testing.assert_allclose(1.5 * np.sin(theta2), np.sin(theta1), atol=1e-14)
np.testing.assert_allclose(np.arcsin(1.5 * np.sin(theta2)), theta1, atol=1e-14)
assert theta1 > critical and 1.5 * np.sin(theta1) > 1
incident = np.array([np.sin(theta1), -np.cos(theta1)])
reflected = np.array([np.sin(theta1), np.cos(theta1)])
transmitted = np.array([np.sin(theta2), -np.cos(theta2)])
for direction in (incident, reflected, transmitted):
np.testing.assert_allclose(np.linalg.norm(direction), 1, atol=1e-14)
np.testing.assert_allclose(np.arccos(-transmitted[1]), theta2, atol=1e-14)
plt.rcParams.update({"font.size": 16, "savefig.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(10.8, 6.1), facecolor="white")
fig.subplots_adjust(left=0.035, right=0.98, top=0.82, bottom=0.18, wspace=0.16)
fig.suptitle("The same 45° incidence, opposite index changes", fontsize=22, y=0.975)
base(axes[0], "Air: n = 1", "Glass: n = 1.5")
axes[0].set_title("Air → glass: refraction", fontsize=18, pad=20)
ray(axes[0], -incident, (0, 0), BLUE)
ray(axes[0], (0, 0), transmitted, GREEN)
axes[0].add_patch(Arc((0, 0), 0.7, 0.7, theta1=90, theta2=135, color=BLUE, lw=1.8))
axes[0].add_patch(Arc((0, 0), 0.7, 0.7, theta1=270, theta2=270 + np.rad2deg(theta2), color=GREEN, lw=1.8))
axes[0].text(-0.40, 0.42, "45°", ha="center", color=BLUE, fontsize=17)
axes[0].text(0.31, -0.47, "28.1255°", color=GREEN, fontsize=17)
axes[0].text(0.1, 0.36, "Transmitted ray\nbends toward normal", fontsize=15, color=GREEN)
base(axes[1], "Glass: n = 1.5", "Air: n = 1")
axes[1].set_title("Glass → air: total reflection", fontsize=18, pad=20)
ray(axes[1], -incident, (0, 0), BLUE)
ray(axes[1], (0, 0), reflected, ORANGE)
axes[1].add_patch(Arc((0, 0), 0.7, 0.7, theta1=90, theta2=135, color=BLUE, lw=1.8))
axes[1].add_patch(Arc((0, 0), 0.7, 0.7, theta1=45, theta2=90, color=ORANGE, lw=1.8))
axes[1].text(-0.40, 0.42, "45°", ha="center", color=BLUE, fontsize=17)
axes[1].text(0.40, 0.42, "45°", ha="center", color=ORANGE, fontsize=17)
axes[1].text(0, -0.46, "No propagating transmitted ray", ha="center", fontsize=15)
axes[1].text(0, -0.68, "Critical angle: 41.8103°", ha="center", fontsize=17)
fig.text(0.5, 0.11, "Angles measured from the normal; paths show direction, not intensity.",
ha="center", fontsize=15)
fig.text(0.5, 0.055, "Left: reflected component omitted. Right: evanescent field omitted.",
ha="center", fontsize=14, color="#555555")
fig.savefig("optical-refraction-rays.png", dpi=100, facecolor="white", transparent=False)
plt.close(fig)
print(f"Verified transmitted angle {np.rad2deg(theta2):.10f}°, critical angle {np.rad2deg(critical):.10f}°,")
print("reverse-path refraction, unit ray directions and total-reflection criterion.")
if __name__ == "__main__":
main()