Skip to content

Commit f743713

Browse files
Mihouclaude
andcommitted
fix(phase): apply the generalized Snell's law in one step
Phase.ray_reaction refracted with plain Snell's law, which drops a ray past the ordinary critical angle, before diffract added the phase gradient. Yu et al. (Science 2011) show the gradient shifts the critical angle: a ray is lost only when |n1 t1 + lambda/(2 pi) grad phi| >= n2. So a phase on glass exiting into air lost steep rays the gradient would transmit. diffract now takes n1 and computes n2 t2 = n1 t1 + m lambda/(2 pi) grad phi directly (Snell's law is the grad phi = 0 case), deciding validity only on |t2|. Docstring cites Yu 2011 and Aieta et al. (Nano Lett. 2012) for the three-dimensional form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 078dbad commit f743713

2 files changed

Lines changed: 42 additions & 24 deletions

File tree

‎deeplens/phase_surface/base_phase.py‎

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ def ray_reaction(self, ray, n1, n2):
177177
"""Trace a ray through the phase surface.
178178
179179
Transforms the ray to local coordinates, intersects it with the plane,
180-
applies refraction then diffraction, and transforms back to global
181-
coordinates.
180+
applies the generalized law of refraction (`diffract`), and transforms
181+
back to global coordinates.
182182
183183
Args:
184184
ray (Ray): Incident ray in global coordinates.
@@ -190,8 +190,7 @@ def ray_reaction(self, ray, n1, n2):
190190
"""
191191
ray = self.to_local_coord(ray)
192192
ray = self.intersect(ray, n1)
193-
ray = self.refract(ray, n1 / n2)
194-
ray = self.diffract(ray, n2=n2)
193+
ray = self.diffract(ray, n1, n2)
195194
ray = self.to_global_coord(ray)
196195
return ray
197196

@@ -241,28 +240,32 @@ def intersect(self, ray, n=1.0):
241240

242241
return ray
243242

244-
def diffract(self, ray, n2=1.0):
245-
"""Apply phase-surface diffraction to a ray.
243+
def diffract(self, ray, n1=1.0, n2=1.0):
244+
"""Refract a ray through the phase surface by the generalized Snell's law.
246245
247246
Two effects are applied:
248247
249248
1. The phase $\\phi$ (in radians) adds to the optical path length as
250-
$m\\,\\phi \\cdot \\lambda / (2\\pi)$, where $\\lambda$ is converted
251-
from [µm] to [mm] internally.
252-
2. The phase gradient bends the ray by the vector grating equation on
253-
the plane (normal $z$),
254-
$n_2 \\mathbf{t}_2 = n_1 \\mathbf{t}_1 + m\\,\\lambda / (2\\pi)\\,\\nabla\\phi$,
255-
for the tangential direction components $\\mathbf{t} = (d_x, d_y)$.
256-
Refraction is already applied, so the tangential components gain
257-
$m\\,\\lambda / (2\\pi n_2)\\,\\nabla\\phi$ and $d_z$ is recomputed so
258-
$|\\mathbf{d}| = 1$. An order with $|\\mathbf{t}_2| \\geq 1$ is
259-
evanescent and the ray is marked invalid. The relation is the same in
260-
both trace directions, so a backward trace retraces the forward path.
249+
$m\\,\\phi \\cdot \\lambda / (2\\pi)$, where $\\lambda$ is the vacuum
250+
wavelength, converted from [µm] to [mm] internally.
251+
2. The phase gradient bends the ray by the generalized law of
252+
refraction [2] in its three-dimensional form [3], on the plane
253+
(normal $z$):
254+
$n_2 \\mathbf{t}_2 = n_1 \\mathbf{t}_1 + m\\,\\lambda / (2\\pi)\\,\\nabla\\phi$
255+
for the tangential direction components $\\mathbf{t} = (d_x, d_y)$,
256+
with $d_z$ recomputed so $|\\mathbf{d}| = 1$. With $\\nabla\\phi = 0$
257+
this is Snell's law, so no separate refraction is applied: the ray
258+
is lost only when $|\\mathbf{t}_2| \\geq 1$, the critical angle the
259+
phase gradient shifts [2], not at the plain-refraction one. The
260+
relation is the same in both trace directions, so a backward trace
261+
retraces the forward path.
261262
262263
Args:
263264
ray (Ray): Ray with position, direction, and wavelength in [µm].
264-
n2 (float, optional): Refractive index of the medium after the surface;
265-
the deflection scales as $1/n_2$. Defaults to 1.0.
265+
n1 (float, optional): Refractive index of the incident medium.
266+
Defaults to 1.0.
267+
n2 (float, optional): Refractive index of the transmission medium.
268+
Defaults to 1.0.
266269
267270
Returns:
268271
ray (Ray): Ray with updated direction `d`, `is_valid` (evanescent
@@ -277,7 +280,8 @@ def diffract(self, ray, n2=1.0):
277280
278281
Reference:
279282
[1] https://support.zemax.com/hc/en-us/articles/1500005489061-How-diffractive-surfaces-are-modeled-in-OpticStudio
280-
[2] Light propagation with phase discontinuities: generalized laws of reflection and refraction. Science 2011.
283+
[2] N. Yu et al., Light propagation with phase discontinuities: generalized laws of reflection and refraction. Science 334, 333 (2011).
284+
[3] F. Aieta et al., Out-of-plane reflection and refraction of light by anisotropic optical antenna metasurfaces with phase discontinuities. Nano Lett. 12, 1702 (2012).
281285
"""
282286
valid = ray.is_valid > 0
283287
order = self.diffraction_order
@@ -290,11 +294,11 @@ def diffract(self, ray, n2=1.0):
290294
)
291295
ray.opl = torch.where(valid.unsqueeze(-1), new_opl, ray.opl)
292296

293-
# Step 2: vector grating equation (see docstring).
297+
# Step 2: generalized Snell's law (see docstring).
294298
dphidx, dphidy = self.dphi_dxy(ray.o[..., 0], ray.o[..., 1])
295-
scale = order * ray.wvln * 1e-3 / (2 * torch.pi * n2)
296-
tx = ray.d[..., 0] + scale * dphidx
297-
ty = ray.d[..., 1] + scale * dphidy
299+
scale = order * ray.wvln * 1e-3 / (2 * torch.pi)
300+
tx = (n1 * ray.d[..., 0] + scale * dphidx) / n2
301+
ty = (n1 * ray.d[..., 1] + scale * dphidy) / n2
298302
# Decide validity on detached values. Invalid rays get a zero tangent so
299303
# the masked sqrt never sees a negative radicand (NaN gradient); the
300304
# clamp keeps its gradient finite as |t| -> 1.

‎test/test_phase_surfaces.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,20 @@ def test_backward_trace_retraces_forward_path(self):
261261
back = s.ray_reaction(back, torch.tensor(1.5), torch.tensor(1.0))
262262
assert torch.allclose(back.d, -d_in, atol=1e-5)
263263

264+
def test_gradient_moves_the_critical_angle(self):
265+
"""Glass to air past the plain critical angle, rescued by the gradient.
266+
267+
Yu et al. 2011: TIR needs |n1 sin(theta1) + lambda/(2 pi) dphi/dy| > n2,
268+
so n1 sin(theta1) = 1.05 with lambda/(2 pi) dphi/dy = -0.1 transmits at
269+
n2 sin(theta2) = 0.95 instead of being lost to plain refraction.
270+
"""
271+
alpha = -0.1 * 2 * math.pi / (WVLN * 1e-3) * 5.0
272+
ray = _oblique_ray(math.degrees(math.asin(0.7)))
273+
out = _grating(alpha).ray_reaction(ray, torch.tensor(1.5), torch.tensor(1.0))
274+
assert out.is_valid.item() == 1
275+
assert out.d[0, 1].item() == pytest.approx(0.95, abs=1e-5)
276+
assert out.d[0, 2].item() == pytest.approx(math.sqrt(1 - 0.95**2), abs=1e-5)
277+
264278
def test_evanescent_order_is_blocked_with_finite_gradient(self):
265279
"""An order with |t| >= 1 is invalid and leaves no NaN gradient."""
266280
s = _grating(1e5) # m lambda/(2 pi) |grad phi| ~ 1.75 > 1

0 commit comments

Comments
 (0)