Gradient Index Lenses

Scott Prahl

Sept 2023

[2]:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches

import pygrin

%config InlineBackend.figure_format='retina'

Pitch of a GRIN lens

A gradient index (GRIN) lens is characterized by a sinusoidal ray path along the lens. A lens is typically characterized by its pitch or the number of sinusoidal periods within the lens.

If the pitch is one (or an integer) then the grin lens acts as a relay and reproduces the light incident on the entrance surface at the exit surface. A half-pitch inverts the pattern. A quarter-pitch lens will collimate a point source or focus a collimated incident beam.

[7]:
n0 = 1.608              # centerline index of refraction
length = 1              # mm
diameter = 0.25            # mm
angle = 40*np.pi/180 # radians
xpos = 0
pitch = 1
radius = diameter/2

# max angle in air
max_angle = pygrin.max_angle(n0,pitch,length,diameter)
# range of launch angles
angles = np.linspace(-max_angle,max_angle,6)
# range of launch angles in grin lens
angles = np.arcsin(np.sin(angles/n0))

plt.subplots(2,1,figsize=(10,10))
plt.subplot(2,1,1)
for angle in angles:
    z,r = pygrin.meridional_curve(n0, pitch, length, xpos, angle)
    plt.plot(z,r, lw=0.5)

plt.plot([0.25*length,0.25*length],[-radius,radius],':k')
plt.plot([0.5*length,0.5*length],[-radius,radius],':k')

plt.xticks([0,0.25,0.5,1.0])
plt.yticks([])
plt.xlabel('Pitch')
plt.title('Full Pitch GRIN Lens, Focus-to-Focus')

plt.subplot(2,1,2)
for xpos in np.linspace(-radius,radius,6):
    z,r = pygrin.meridional_curve(n0, pitch, length, xpos, 0)
    plt.plot(z,r, lw=0.5)

plt.plot([0.25*length,0.25*length],[-radius,radius],':k')
plt.plot([0.5*length,0.5*length],[-radius,radius],':k')

plt.xticks([0,0.25,0.5,1.0])
plt.yticks([])
plt.xlabel('Pitch')
plt.title('Full Pitch GRIN Lens, Parallel-to-Parallel')
plt.tight_layout()  # Ensures proper spacing

#plt.savefig('pitch.png', dpi=300)
plt.show()

_images/01-grin-lens-basics_3_0.png

Quarter Pitch

This is the typical example because collimated light is focused to a point, or conversely, a point source is collimated. Here we see an example the former.

[3]:
pitch = 0.25
n0 = 1.608              # centerline index of refraction
length = 5              # mm
diameter = 2            # mm
angle = 0*np.pi/180     # radians

pygrin.plot_principal_planes(n0, pitch, length, diameter)
xpos = np.linspace(-diameter/2,diameter/2,9)
for pos in xpos:
    z,r = pygrin.meridional_curve(n0, pitch, length, pos, angle)
    plt.plot(z,r, color='blue',lw=0.5)

plt.rcParams["figure.figsize"] = [10,3]
plt.axis('off')
plt.show()
_images/01-grin-lens-basics_5_0.png

This shows light being collimated from a point source

[4]:
pitch = 0.25
n0 = 1.608              # centerline index of refraction
length = 5              # mm
diameter = 2            # mm
xpos = 0                # mm

pygrin.plot_principal_planes(n0, pitch, length, diameter)

# max angle in air
max_angle = pygrin.max_angle(n0,pitch,length,diameter)
# range of launch angles
angles = np.linspace(-max_angle,max_angle,9)
# range of launch angles in grin lens
angles = np.arcsin(np.sin(angles/n0))

for angle in angles:
    z,r = pygrin.meridional_curve(n0, pitch, length, xpos, angle)
    plt.plot(z,r, color='blue',lw=0.5)

plt.rcParams["figure.figsize"] = [10,3]
plt.axis('off')
plt.show()
_images/01-grin-lens-basics_7_0.png

Here is a 4f system. Here the source on the left is on focal distance from the front face and the

[5]:
# 4f system
pitch = 0.25
n0 = 1.608              # centerline index of refraction
length = 5              # mm
diameter = 2            # mm
zobj = pygrin.EFL(n0,pitch,length)
xpos = np.linspace(-diameter/4,diameter/4,9)

pygrin.plot_principal_planes(n0, pitch, length, diameter)

for pos in xpos:
    z,r = pygrin.full_meridional_curve(n0, pitch, length, -zobj, 0.0, pos)
    plt.plot(z,r, color='blue',lw=0.5)

plt.rcParams["figure.figsize"] = [10,3]
plt.axis('off')
plt.show()
_images/01-grin-lens-basics_9_0.png

Finally, this shows that collimated light incident at an angle on the lens will be imaged to a point off-axis.

[6]:
# off axis launch
pitch = 0.25
n0 = 1.608              # centerline index of refraction
length = 5           # mm
diameter = 2          # mm
zobj = pygrin.EFL(n0,pitch,length)
xpos = np.linspace(-diameter/4,diameter/4,9)

pygrin.plot_principal_planes(n0, pitch, length, diameter)

for pos in xpos:
    z,r = pygrin.full_meridional_curve(n0, pitch, length, -zobj, pos-0.1, pos)
    plt.plot(z,r, color='blue',lw=0.5)

plt.rcParams["figure.figsize"] = [10,3]
plt.axis('off')
plt.show()
_images/01-grin-lens-basics_11_0.png

Half Pitch

Here we show that all the rays hitting the front surface at a point are imaged to the same point on the exit surface — but inverted.

The prinicipal planes are not drawn because the effective focal length is infinite.

[7]:
pitch = 0.5
n0 = 1.608              # centerline index of refraction
length = 5              # mm
diameter = 2            # mm
xpos = 0.5                # mm

pygrin.plot_principal_planes(n0, pitch, length, diameter)

# max angle in air
max_angle = pygrin.max_angle(n0,pitch,length,diameter)
# range of launch angles
angles = np.linspace(-max_angle,max_angle,9)
# range of launch angles in grin lens
angles = np.arcsin(np.sin(angles/n0))

for angle in angles:
    z,r = pygrin.meridional_curve(n0, pitch, length, xpos, angle)
    plt.plot(z,r, color='blue',lw=0.5)

plt.rcParams["figure.figsize"] = [10,3]
plt.axis('off')
plt.show()
_images/01-grin-lens-basics_13_0.png

Index of Refraction Across Lens

Parabolic Profile

[8]:
n0 = 1.608
pitch = 0.25
length = 5    # mm
diameter = 2
r = np.linspace(-diameter/2,diameter/2,50)

plt.plot(r,pygrin.parabolic_profile_index(n0,pitch,length,r))
plt.plot([0,0],[1.53,n0],':k')
plt.xlabel('Radius (mm)')
plt.ylabel('Index of Refraction')
plt.title(r'pitch=%.2f, n$_0$=%.3f, length=%.2fmm'%(pitch,n0,length))
plt.rcParams["figure.figsize"] = [6,4]

plt.show()
_images/01-grin-lens-basics_16_0.png

Hyperbolic Secant Profile

This has a few advantages over the parabolic profile: the propagation for a HS grin lens can be solved exactly and there aren’t any aberrations for meridional rays.

As you can see below, the HS profile can be quite close to the parabolic profile.

[9]:
n0 = 1.608
pitch = 0.19
length = 5    # mm
diameter = 2
gradient = pygrin.gradient(pitch,length)
alpha = 1.303*gradient

r = np.linspace(-diameter/2,diameter/2,50)

plt.subplots(2,1, sharex=True,figsize=(6,6))
plt.subplot(2,1,1)
n_p = pygrin.parabolic_profile_index(n0,pitch,length,r)
n_s = pygrin.hyperbolic_secant_profile_index(n0,alpha,r)
plt.plot(r,n_p)
plt.plot(r,n_s)
plt.xticks([])
plt.ylabel('Index of Refraction')
plt.title(r'pitch=%.2f, n$_0$=%.3f, length=%.2fmm'%(pitch,n0,length))
plt.rcParams["figure.figsize"] = [6,4]

plt.subplot(2,1,2)
plt.plot(r,n_p-n_s)

plt.xlabel('Radius (mm)')
plt.ylabel('Difference')
plt.plot([-1,1],[0,0],':k')

plt.show()
_images/01-grin-lens-basics_18_0.png

Catalog Examples

Grin Lens from ancient Melles Griot Catalog, 4.67 line 2

Part number LGS-0.25-1.0-2.58-633

[10]:
n = 1.608
gradient = 0.608
length = 2.58
diameter = 1

pitch = pygrin.period(gradient, length)
print('expected pitch = 0.25,           calculated %.2f'%pitch)

efl = pygrin.EFL(n,pitch,length)
na = pygrin.NA(n,pitch,length,diameter)
print('expected NA = 0.46,              calculated %.2f'%na)

angle = pygrin.max_angle(n,pitch,length,diameter)
print('expected full accept angle = 55°, calculated %.0f°'%(2*angle*180/np.pi))
expected pitch = 0.25,           calculated 0.25
expected NA = 0.46,              calculated 0.46
expected full accept angle = 55°, calculated 54°
[11]:
r = np.linspace(-0.5,0.5,50)
plt.plot(r,pygrin.parabolic_profile_index(n,pitch,length,r))
plt.xlabel('Radius (mm)')
plt.ylabel('Index of Refraction')
plt.title('pitch=%.3f, n0=%.3f, L=%.2fmm'%(pitch,n,length))
plt.show()
_images/01-grin-lens-basics_22_0.png
[12]:
pygrin.plot_principal_planes(n, pitch, length, diameter)
xpos = np.linspace(-diameter/2,diameter/2,7)
for pos in xpos:
    z,r = pygrin.meridional_curve(n, pitch, length, pos, 0*np.pi/180)
    plt.plot(z,r, color='blue',lw=0.5)

plt.title('Melles-Griot LGS-0.25-1.0-2.58-633')
plt.show()
_images/01-grin-lens-basics_23_0.png

Grin Lens from ancient Melles Griot Catalog, 4.67 line 5

Part number LGE-0.29-1.8-5.37-633

[13]:
n = 1.608
gradient = 0.339
length = 5.37
diameter = 1.8

pitch = pygrin.period(gradient, length)
print('expected pitch = 0.29,            calculated %.2f'%pitch)

ffl = pygrin.FFL(n,pitch,length)
efl = pygrin.EFL(n,pitch,length)
print('expected FFL = 0.46,              calculated %.2f'%ffl)

na = pygrin.NA(n,pitch,length,diameter)
print('expected NA = 0.46,               calculated %.2f'%na)

angle = pygrin.max_angle(n,pitch,length,diameter)
print('expected full accept angle = 55°, calculated %.0f°'%(2*angle*180/np.pi))

print('working distance = %.2f mm'%(efl-ffl))
expected pitch = 0.29,            calculated 0.29
expected FFL = 0.46,              calculated 0.47
expected NA = 0.46,               calculated 0.46
expected full accept angle = 55°, calculated 55°
working distance = 1.43 mm
[14]:
pygrin.plot_principal_planes(n, pitch, length, diameter)
xpos = np.linspace(-diameter/4, diameter/4,8)
for pos in xpos:
    z,r = pygrin.full_meridional_curve(n, pitch, length, ffl-efl, 0, pos)
    plt.plot(z,r, color='blue', lw=0.5)

plt.title('Melles-Griot LGE-0.29-1.8-5.37-633')

plt.show()
_images/01-grin-lens-basics_26_0.png

Riedl, page 96

[15]:
n = 1.5834
gradient = np.sqrt(0.1067)
length = 4
diameter = 1.8

pitch = pygrin.period(gradient, length)
print('expected pitch = 0.207,          calculated %.3f'%pitch)

efl = pygrin.EFL(n,pitch,length)
print('expected EFL = 2.00,             calculated %.2f'%efl)

ffl = pygrin.FFL(n,pitch,length)
print('expected FFL = -0.52,            calculated %.2f'%ffl)

bfl = pygrin.BFL(n,pitch,length)
print('expected BFL = 4.52,             calculated %.2f'%bfl)

na = pygrin.NA(n,pitch,length,diameter)
print('expected NA = 0.46,              calculated %.2f'%na)

angle = pygrin.max_angle(n,pitch,length,diameter)
print('expected full accept angle = 55°, calculated %.0f°'%(2*angle*180/np.pi))

print('working distance = %.2f mm'%(-ffl+efl))
expected pitch = 0.207,          calculated 0.208
expected EFL = 2.00,             calculated 2.00
expected FFL = -0.52,            calculated -0.52
expected BFL = 4.52,             calculated 4.52
expected NA = 0.46,              calculated 0.44
expected full accept angle = 55°, calculated 52°
working distance = 2.53 mm
[16]:
pygrin.plot_principal_planes(n, pitch, length, diameter)
xpos = np.linspace(-diameter/2, diameter/2,7)
for pos in xpos:
    z,r = pygrin.full_meridional_curve(n, pitch, length, ffl-efl, pos, pos)
    plt.plot(z,r, color='blue', lw=0.5)

plt.show()
_images/01-grin-lens-basics_29_0.png

Grin Lenses at Oregon Tech

Oriel 41425

Designed for 632.8nm

[17]:
n = 1.608
pitch = 0.25
length = 6.28
diameter = 2

gradient = pygrin.gradient(pitch, length)
print('expected gradient = 0.250,        calculated %.3f'%pitch)

efl = pygrin.EFL(n,pitch,length)
#print('expected EFL = 2.00,              calculated %.2f'%efl)

ffl = pygrin.FFL(n,pitch,length)
#print('expected FFL = -0.52,             calculated %.2f'%ffl)

bfl = pygrin.BFL(n,pitch,length)
#print('expected BFL = 4.52,              calculated %.2f'%bfl)

na = pygrin.NA(n,pitch,length,diameter)
print('expected NA = 0.38,               calculated %.2f'%na)

angle = pygrin.max_angle(n,pitch,length,diameter)
print('expected full accept angle = 45°, calculated %.0f°'%(2*angle*180/np.pi))

print('working distance = %.2f mm'%(efl-ffl))
expected gradient = 0.250,        calculated 0.250
expected NA = 0.38,               calculated 0.38
expected full accept angle = 45°, calculated 45°
working distance = 2.49 mm
[18]:
pygrin.plot_principal_planes(n, pitch, length, diameter)
xpos = np.linspace(-diameter/2, diameter/2,7)
for pos in xpos:
    z,r = pygrin.full_meridional_curve(n, pitch, length, ffl-efl, pos, pos)
    plt.plot(z,r, color='blue', lw=0.5)

plt.title('Oriel 41425')

plt.show()
_images/01-grin-lens-basics_33_0.png

Oriel 41440

Designed for 632.8nm

[19]:
n = 1.608
pitch = 0.29
length = 5.35
diameter = 2

gradient = pygrin.gradient(pitch, length)
print('expected gradient = 0.290,        calculated %.3f'%pitch)

efl = pygrin.EFL(n,pitch,length)
#print('expected EFL = 2.00,              calculated %.2f'%efl)

ffl = pygrin.FFL(n,pitch,length)
#print('expected FFL = -0.52,             calculated %.2f'%ffl)

bfl = pygrin.BFL(n,pitch,length)
#print('expected BFL = 4.52,              calculated %.2f'%bfl)

na = pygrin.NA(n,pitch,length,diameter)
print('expected NA = 0.38,               calculated %.2f'%na)

angle = pygrin.max_angle(n,pitch,length,diameter)
print('expected full accept angle = 60°, calculated %.0f°'%(2*angle*180/np.pi))

print('working distance = %.2f mm'%(efl-ffl))
expected gradient = 0.290,        calculated 0.290
expected NA = 0.38,               calculated 0.50
expected full accept angle = 60°, calculated 60°
working distance = 1.42 mm
[20]:
pygrin.plot_principal_planes(n, pitch, length, diameter)
xpos = np.linspace(-diameter/4, diameter/4,7)
for pos in xpos:
    z,r = pygrin.full_meridional_curve(n, pitch, length, ffl-efl, 0, pos)
    plt.plot(z,r, color='blue', lw=0.5)

plt.title('Oriel 41440')
plt.show()
_images/01-grin-lens-basics_36_0.png

Newport FK - GR29

Designed for 850nm

[21]:
n = 1.608
pitch = 0.29
length = 5.49
diameter = 1.8

gradient = pygrin.gradient(pitch, length)
print('expected gradient = 0.290,        calculated %.3f'%pitch)

efl = pygrin.EFL(n,pitch,length)
print('expected EFL = 1.95,              calculated %.2f'%efl)

ffl = pygrin.FFL(n,pitch,length)
#print('expected FFL = -0.52,             calculated %.2f'%ffl)

bfl = pygrin.BFL(n,pitch,length)
#print('expected BFL = 4.52,              calculated %.2f'%bfl)

na = pygrin.NA(n,pitch,length,diameter)
print('expected NA = 0.46,               calculated %.2f'%na)

angle = pygrin.max_angle(n,pitch,length,diameter)
print('expected full accept angle = 60°, calculated %.0f°'%(2*angle*180/np.pi))

print('working distance = %.2f mm'%(efl-ffl))
expected gradient = 0.290,        calculated 0.290
expected EFL = 1.95,              calculated 1.93
expected NA = 0.46,               calculated 0.45
expected full accept angle = 60°, calculated 53°
working distance = 1.45 mm
[22]:
pygrin.plot_principal_planes(n, pitch, length, diameter)
xpos = np.linspace(-diameter/4, diameter/4,7)
for pos in xpos:
    z,r = pygrin.full_meridional_curve(n, pitch, length, ffl-efl, 0, pos)
    plt.plot(z,r, color='blue', lw=0.5)

plt.title('Newport FK-GR29')
plt.show()
_images/01-grin-lens-basics_39_0.png