Fermat's Principle

Finding the Path of Least Time with Python

What is Fermat’s Principle?

Fermat’s Principle states that light travels between two points along the path that takes the least time (more precisely, an extremal time). In a medium with refractive index $n$, light travels at speed $v = c/n$, and the total travel time along a path is:

$$T = \int_A^B \frac{n(\mathbf{r})}{c}, ds$$

When light crosses a boundary between two media, minimizing this time functional leads directly to Snell’s Law:

$$n_1 \sin\theta_1 = n_2 \sin\theta_2$$

Rather than deriving this law analytically, this article demonstrates it computationally — we let a numerical optimizer discover the fastest path on its own, and then check that it reproduces Snell’s Law.

Setting Up a Concrete Example

Consider a flat interface at $z = 0$, separating two media:

  • Medium 1 (e.g. air, $n_1 = 1.00$) occupies $z > 0$
  • Medium 2 (e.g. water, $n_2 = 1.33$) occupies $z < 0$

Light starts at point $A = (0, 0, 5)$ in medium 1 and must reach point $B = (8, 3, -4)$ in medium 2. It crosses the interface at some unknown point $P = (x, y, 0)$.

The total travel time as a function of the crossing point is:

$$T(x,y) = \frac{n_1}{c}\sqrt{x^2 + y^2 + h_1^2} ;+; \frac{n_2}{c}\sqrt{(\Delta x - x)^2 + (\Delta y - y)^2 + h_2^2}$$

where $h_1 = 5$, $h_2 = 4$, $\Delta x = 8$, $\Delta y = 3$. The goal is to numerically find the $(x,y)$ that minimizes $T$, and confirm that the resulting angles satisfy Snell’s Law.

Full Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import numpy as np
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (enables 3D projection)

# =========================================================
# 1. Physical setting
# Light travels from point A (medium 1, e.g. air)
# to point B (medium 2, e.g. water), crossing a flat
# interface located at z = 0.
# =========================================================
c = 299792458.0 # speed of light in vacuum [m/s]
n1 = 1.00 # refractive index of medium 1 (air)
n2 = 1.33 # refractive index of medium 2 (water)

A = np.array([0.0, 0.0, 5.0]) # source point (z > 0 -> medium 1)
B = np.array([8.0, 3.0, -4.0]) # destination point (z < 0 -> medium 2)

# =========================================================
# 2. Travel time as a function of the crossing point P=(x,y,0)
# T(x,y) = n1/c * |AP| + n2/c * |PB|
# =========================================================
def travel_time(p):
x, y = p
d1 = np.sqrt(x**2 + y**2 + A[2]**2)
d2 = np.sqrt((B[0]-x)**2 + (B[1]-y)**2 + B[2]**2)
return n1 * d1 / c + n2 * d2 / c

p0 = np.array([A[0] + (B[0]-A[0])*0.5, A[1] + (B[1]-A[1])*0.5]) # straight-line initial guess

result = minimize(travel_time, p0, method='BFGS',
options={'gtol': 1e-12, 'maxiter': 2000})

Px, Py = result.x
P = np.array([Px, Py, 0.0])
T_min = result.fun

# =========================================================
# 3. Verify Snell's law along the found path
# =========================================================
horiz_in = np.hypot(Px - A[0], Py - A[1])
horiz_out = np.hypot(B[0] - Px, B[1] - Py)
dist_in = np.hypot(horiz_in, A[2])
dist_out = np.hypot(horiz_out, B[2])

sin_theta1 = horiz_in / dist_in
sin_theta2 = horiz_out / dist_out

ratio_B = B[1] / B[0]
ratio_P = Py / Px

print("========== Fermat's Principle: numerical result ==========")
print(f"Crossing point P : ({Px:.6f}, {Py:.6f}, 0.000000)")
print(f"Minimum travel time T_min : {T_min*1e9:.6f} ns")
print(f"n1 * sin(theta1) : {n1*sin_theta1:.6f}")
print(f"n2 * sin(theta2) : {n2*sin_theta2:.6f}")
print(f"slope y/x of B : {ratio_B:.6f}")
print(f"slope y/x of P (should match): {ratio_P:.6f}")
print("============================================================")

# =========================================================
# 4. Compare with a naive straight-line path (no refraction)
# =========================================================
T_straight = travel_time(p0)
print(f"\nTravel time of the naive straight-line path : {T_straight*1e9:.6f} ns")
print(f"Travel time of the Fermat (fastest) path : {T_min*1e9:.6f} ns")
print(f"Time saved by refraction : {(T_straight-T_min)*1e9:.6f} ns")

# =========================================================
# 5. Vectorised grid for visualising T(x,y) as a surface
# (numpy broadcasting instead of double for-loops => fast)
# =========================================================
grid_n = 300
xs = np.linspace(-4, 12, grid_n)
ys = np.linspace(-4, 8, grid_n)
X, Y = np.meshgrid(xs, ys)

D1 = np.sqrt(X**2 + Y**2 + A[2]**2)
D2 = np.sqrt((B[0]-X)**2 + (B[1]-Y)**2 + B[2]**2)
T = n1 * D1 / c + n2 * D2 / c

# =========================================================
# 6. Figure 1: 3D visualisation of the actual light path
# =========================================================
fig1 = plt.figure(figsize=(9, 7))
ax1 = fig1.add_subplot(111, projection='3d')

plane_x, plane_y = np.meshgrid(np.linspace(-4, 12, 2), np.linspace(-4, 8, 2))
ax1.plot_surface(plane_x, plane_y, np.zeros_like(plane_x),
color='lightblue', alpha=0.3, edgecolor='none')

ax1.plot([A[0], P[0]], [A[1], P[1]], [A[2], P[2]], 'r-', linewidth=2.5, label='Ray in medium 1')
ax1.plot([P[0], B[0]], [P[1], B[1]], [P[2], B[2]], 'b-', linewidth=2.5, label='Ray in medium 2')

ax1.scatter(*A, color='black', s=60)
ax1.scatter(*B, color='black', s=60)
ax1.scatter(*P, color='green', s=80, label='Crossing point P (fastest)')

ax1.text(*A, ' A', fontsize=11)
ax1.text(*B, ' B', fontsize=11)
ax1.text(*P, ' P', fontsize=11)

ax1.set_xlabel('X')
ax1.set_ylabel('Y')
ax1.set_zlabel('Z')
ax1.set_title("Fermat's Principle: fastest path from A to B")
ax1.legend()
plt.tight_layout()
plt.show()

# =========================================================
# 7. Figure 2: 3D surface of T(x,y) with the minimum marked
# =========================================================
fig2 = plt.figure(figsize=(9, 7))
ax2 = fig2.add_subplot(111, projection='3d')

surf = ax2.plot_surface(X, Y, T*1e9, cmap='viridis', alpha=0.85, linewidth=0, antialiased=True)
ax2.scatter(Px, Py, T_min*1e9, color='red', s=80, label='Minimum (Fermat point)')

ax2.set_xlabel('x (crossing point)')
ax2.set_ylabel('y (crossing point)')
ax2.set_zlabel('Travel time [ns]')
ax2.set_title('Travel time T(x, y) for every possible crossing point')
fig2.colorbar(surf, shrink=0.6, aspect=12, label='Travel time [ns]')
ax2.legend()
plt.tight_layout()
plt.show()

# =========================================================
# 8. Figure 3: 2D slice through the minimum (easy-to-read cut)
# =========================================================
direction = np.array([B[0], B[1]]) / np.hypot(B[0], B[1])
t_vals = np.linspace(-4, 12, 400)
line_x = direction[0] * t_vals
line_y = direction[1] * t_vals
T_line = n1*np.sqrt(line_x**2 + line_y**2 + A[2]**2)/c + \
n2*np.sqrt((B[0]-line_x)**2 + (B[1]-line_y)**2 + B[2]**2)/c

fig3, ax3 = plt.subplots(figsize=(8, 5))
ax3.plot(t_vals, T_line*1e9, color='navy', linewidth=2)
ax3.axvline(np.hypot(Px, Py), color='red', linestyle='--', label='Fermat minimum')
ax3.set_xlabel('Distance along the plane of incidence')
ax3.set_ylabel('Travel time [ns]')
ax3.set_title('Travel time along the plane of incidence (2D cut of the 3D surface)')
ax3.legend()
ax3.grid(alpha=0.3)
plt.tight_layout()
plt.show()

Code Walkthrough

Section 1–2 (Physical setup and time function): We define two points $A$ and $B$ straddling a flat interface at $z=0$, along with the refractive indices of each medium. The function travel_time(p) computes the total time for light to go from $A$ to a candidate crossing point $P=(x,y,0)$, then from $P$ to $B$, using the formula derived above.

Section 2 (Optimization): Instead of a brute-force search, we use scipy.optimize.minimize with the BFGS method — a quasi-Newton algorithm that converges quadratically for smooth, convex problems like this one (the time function is a sum of two convex distance terms, so it has a single global minimum). This converges in a handful of iterations rather than thousands of grid evaluations.

Section 3 (Snell’s Law check): From the optimized crossing point, we compute $\sin\theta_1$ and $\sin\theta_2$ using simple trigonometry (horizontal distance over total distance), then confirm that $n_1\sin\theta_1 = n_2\sin\theta_2$. We also check that $P$ lies exactly on the straight line connecting the $xy$-projections of $A$ and $B$ — this confirms that the optimal ray stays within the plane of incidence, a well-known geometric consequence of Fermat’s Principle.

Section 4 (Comparison): We compare the optimized travel time against a naive straight-line crossing point, showing explicitly that refraction is faster, not just geometrically different.

Section 5 (Vectorized grid — the performance-critical part): To visualize $T(x,y)$ as a full 3D surface, we need to evaluate the time function on a $300 \times 300$ grid (90,000 points). Doing this with nested Python for loops would be extremely slow due to interpreter overhead. Instead, we use NumPy broadcasting with np.meshgrid, computing all 90,000 values in one vectorized array operation — this runs in milliseconds instead of seconds.

Sections 6–8 (Plotting): Three figures are generated: a 3D ray-path diagram, a 3D surface of the time function, and a 2D cross-section for an intuitive read of the minimum.

Results

========== Fermat's Principle: numerical result ==========
Crossing point P             : (5.410675, 2.029003, 0.000000)
Minimum travel time T_min    : 47.062861 ns
n1 * sin(theta1)             : 0.756215
n2 * sin(theta2)             : 0.756341
slope y/x of B               : 0.375000
slope y/x of P (should match): 0.375000
============================================================

Travel time of the naive straight-line path : 47.900133 ns
Travel time of the Fermat (fastest) path    : 47.062861 ns
Time saved by refraction                    : 0.837272 ns

Visualizing the Fastest Path

Figure 1 — The 3D Light Path

This figure shows the actual geometry: point $A$ above the interface, point $B$ below it, and the bent ray path through the crossing point $P$ found by the optimizer. The pale blue plane represents the interface between the two media. Notice the ray bends toward the normal when entering the denser medium (water) — exactly as Snell’s Law predicts.

Figure 2 — The Time Surface (3D)

This is the most instructive plot: it renders $T(x,y)$ as a full 3D bowl-shaped surface over every conceivable crossing point, not just the correct one. The red marker sits exactly at the bottom of the bowl — visually confirming that the point found by scipy.optimize.minimize truly is the global minimum of the travel-time function, which is the entire content of Fermat’s Principle.

Figure 3 — 2D Cross-Section Through the Minimum

Since the 3D bowl can be hard to read precisely, this figure slices the surface along the plane of incidence, producing an ordinary 2D curve. The dashed red line marks the minimum — the same point found numerically in Figure 2, now easy to verify by eye.

Why This Matters

What makes this example powerful is that we never told the program about Snell’s Law. We only told it: “minimize the travel time.” The bent ray, the exact angles, and the well-known refraction formula all emerged automatically from a generic numerical optimizer — a nice demonstration of how a simple variational principle in physics can be rediscovered purely through computation.