Advanced Design

Lithographic Projection System

Design a complex high-NA system where polarization and coatings are mission-critical.

AdvancedAdvanced Optical DesignNumPy backend20 min read

Introduction

This tutorial demonstrates how Optiland can be used to analyze and optimize a lithographic projection lens. The starting point for this design is based on U.S. Patent #5831776.

Core concepts used

materials.IdealMaterial(n=1.5084)
Defines a material with a fixed index for a specific wavelength (248nm DUV), ignoring dispersion for the optimization phase.
obj_space_telecentric = True
Forces chief rays to be parallel to the optical axis in object space—a critical requirement for lithography scanners.
aperture_type='objectNA'
Defines the system aperture in terms of Numerical Aperture (NA) rather than physical diameter.
OPD_difference
The primary merit function operand used here to drive the large-scale optimization into the diffraction limit.

Step-by-step build

1

Import NumPy and Optiland modules

python
import numpy as np

from optiland import analysis, materials, mtf, optic, optimization, wavefront
2

Define the DUV lithographic lens

The lens is designed for a wavelength of 248 nm and only uses silica lenses. Note that the lens is telecentric in object space. This only forces the chief ray in object space to be parallel with the optical axis. It does not directly enforce that the chief ray passes through the center of the aperture stop.

python
lens = optic.Optic()

# We define SiO2 with the index of refraction at 248 nm
SiO2 = materials.IdealMaterial(n=1.5084, k=0)

# Define all surfaces
lens.add_surface(index=0, radius=np.inf, thickness=110.85883544)
lens.add_surface(index=1, radius=-737.7847, thickness=27.484, material=SiO2)
lens.add_surface(index=2, radius=-235.2891, thickness=0.916)
lens.add_surface(index=3, radius=211.1786, thickness=36.646, material=SiO2)
lens.add_surface(index=4, radius=-461.3986, thickness=0.916)
lens.add_surface(index=5, radius=412.6778, thickness=21.071, material=SiO2)
lens.add_surface(index=6, radius=160.5391, thickness=16.197)
lens.add_surface(index=7, radius=-604.1283, thickness=7.215, material=SiO2)
lens.add_surface(index=8, radius=218.1877, thickness=23.941)
lens.add_surface(index=9, radius=-3586.063, thickness=11.978, material=SiO2)
lens.add_surface(index=10, radius=251.8168, thickness=47.506)
lens.add_surface(index=11, radius=-85.2817, thickness=11.961, material=SiO2)
lens.add_surface(index=12, radius=584.8597, thickness=9.968)
lens.add_surface(index=13, radius=4074.801, thickness=35.291, material=SiO2)
lens.add_surface(index=14, radius=-162.0185, thickness=0.923)
lens.add_surface(index=15, radius=629.544, thickness=41.227, material=SiO2)
lens.add_surface(index=16, radius=-226.7397, thickness=0.916)
lens.add_surface(index=17, radius=522.2739, thickness=27.842, material=SiO2)
lens.add_surface(index=18, radius=-582.424, thickness=0.916)
lens.add_surface(index=19, radius=423.729, thickness=22.904, material=SiO2)
lens.add_surface(index=20, radius=-1385.36, thickness=0.916, is_stop=True)
lens.add_surface(index=21, radius=212.039, thickness=33.646, material=SiO2)
lens.add_surface(index=22, radius=802.3695, thickness=55.304)
lens.add_surface(index=23, radius=-776.5697, thickness=8.703, material=SiO2)
lens.add_surface(index=24, radius=106.1728, thickness=24.09)
lens.add_surface(index=25, radius=-200.683, thickness=11.452, material=SiO2)
lens.add_surface(index=26, radius=311.8264, thickness=59.54)
lens.add_surface(index=27, radius=-77.2276, thickness=11.772, material=SiO2)
lens.add_surface(index=28, radius=2317.8032, thickness=11.862)
lens.add_surface(index=29, radius=-290.8859, thickness=22.904, material=SiO2)
lens.add_surface(index=30, radius=-148.3577, thickness=1.373)
lens.add_surface(index=31, radius=-5658.5043, thickness=41.227, material=SiO2)
lens.add_surface(index=32, radius=-151.9858, thickness=0.916)
lens.add_surface(index=33, radius=678.1005, thickness=32.981, material=SiO2)
lens.add_surface(index=34, radius=-358.554, thickness=0.916)
lens.add_surface(index=35, radius=264.2734, thickness=32.814, material=SiO2)
lens.add_surface(index=36, radius=2309.6884, thickness=0.916)
lens.add_surface(index=37, radius=171.2681, thickness=29.015, material=SiO2)
lens.add_surface(index=38, radius=364.7765, thickness=0.918)
lens.add_surface(index=39, radius=113.37, thickness=76.259, material=SiO2)
lens.add_surface(index=40, radius=78.6982, thickness=54.304)
lens.add_surface(index=41, radius=49.5443, thickness=18.65, material=SiO2)
lens.add_surface(index=42, radius=109.8136, thickness=13.07647896)
lens.add_surface(index=43, radius=np.inf)

# Define the aperture (the original NA was 0.15, but we reduce it slightly to avoid
# negative edge thicknesses)
lens.set_aperture(aperture_type="objectNA", value=0.133)

# Define the field
lens.set_field_type(field_type="object_height")
lens.add_field(y=0)
lens.add_field(y=32)
lens.add_field(y=48)

# Define the wavelength
lens.add_wavelength(value=0.248, is_primary=True)

# Specify that the lens is object-space telecentric
lens.obj_space_telecentric = True

# Move last surface to the paraxial image plane
lens.image_solve()
3

Draw the lens layout

python
lens.draw(figsize=(12, 3))
Step
4

Assess initial MTF performance

  1. Initial Performance Assessment

Let's see how close the lens is to diffraction-limited by plotting the MTF:

python
lens_mtf = mtf.FFTMTF(lens)
lens_mtf.view(add_reference=True)
Step
5

Set up the optimization problem

Clearly, the lens is far from the diffraction limit.

  1. Optimization

We now wish to make the lens diffraction-limited. Let's optimize the imaging performance by allowing all lens radii to vary.

python
problem = optimization.OptimizationProblem()

# Add focal length operand
problem.add_operand(
 operand_type="f2", target=494, weight=1, input_data={"optic": lens}
)

# Add OPD operands for imaging quality
for field in lens.fields.get_field_coords():
 input_data = {
     "optic": lens,
     "Hx": field[0],
     "Hy": field[1],
     "num_rays": 5,
     "wavelength": 0.248,
     "distribution": "gaussian_quad",
 }
 problem.add_operand(
     operand_type="OPD_difference",
     target=0,
     weight=10,
     input_data=input_data,
 )

# Allow the radii of curvature of all surfaces to vary
for k in range(1, lens.surface_group.num_surfaces - 1):
 problem.add_variable(
     lens,
     "radius",
     surface_number=k,
     min_val=-10000,
     max_val=10000,
 )

# Print current merit function value
problem.merit_info()
6

Run the optimizer

We now run the standard optimizer. Note that this may take several minutes for a termination tolerance of 1e-9, but this should result in a high-performing lens.

python
optimizer = optimization.OptimizerGeneric(problem)
res = optimizer.optimize(tol=1e-9)
7

Print post-optimization merit function

Printing the merit function result, we can see an improvement of ā‰ˆ98%.

python
problem.merit_info()
8

Plot the optimized MTF

Let's again view the MTF:

python
lens_mtf = mtf.FFTMTF(lens)
lens_mtf.view(add_reference=True)
Step
9

Compute the OPD wavefront map

This is a significant improvement over the initial design. We plot the diffraction limit as a reference.

Let's generate a few visualizations to show the final performance of the system. We will compute:

  • Wavefront OPD map for the (Hx, Hy) = (0, 1) field & corresponding standard Zernike coefficients

  • Standard spot diagram

  • Ray aberration fans

    python
    opd = wavefront.OPD(lens, field=(0, 1), wavelength=0.248)
     opd.view(projection="2d", num_points=256)
    Step
10

Extract Zernike wavefront coefficients

python
print("Zernike Standard Coefficients:")
zernike = wavefront.ZernikeOPD(
 lens,
 (0, 1),
 0.55,
 zernike_type="standard",
 num_terms=21,
)

for k in range(len(zernike.coeffs)):
 print(f"\tZ{k + 1}: {zernike.coeffs[k]:.8f}")
11

View the spot diagram

python
spot = analysis.SpotDiagram(lens)
spot.view()
Step
12

Plot the ray aberration fans

python
fan = analysis.RayFan(lens)
fan.view()
Step
Show full code listing
python
import numpy as np

from optiland import analysis, materials, mtf, optic, optimization, wavefront

lens = optic.Optic()

# We define SiO2 with the index of refraction at 248 nm
SiO2 = materials.IdealMaterial(n=1.5084, k=0)

# Define all surfaces
lens.add_surface(index=0, radius=np.inf, thickness=110.85883544)
lens.add_surface(index=1, radius=-737.7847, thickness=27.484, material=SiO2)
lens.add_surface(index=2, radius=-235.2891, thickness=0.916)
lens.add_surface(index=3, radius=211.1786, thickness=36.646, material=SiO2)
lens.add_surface(index=4, radius=-461.3986, thickness=0.916)
lens.add_surface(index=5, radius=412.6778, thickness=21.071, material=SiO2)
lens.add_surface(index=6, radius=160.5391, thickness=16.197)
lens.add_surface(index=7, radius=-604.1283, thickness=7.215, material=SiO2)
lens.add_surface(index=8, radius=218.1877, thickness=23.941)
lens.add_surface(index=9, radius=-3586.063, thickness=11.978, material=SiO2)
lens.add_surface(index=10, radius=251.8168, thickness=47.506)
lens.add_surface(index=11, radius=-85.2817, thickness=11.961, material=SiO2)
lens.add_surface(index=12, radius=584.8597, thickness=9.968)
lens.add_surface(index=13, radius=4074.801, thickness=35.291, material=SiO2)
lens.add_surface(index=14, radius=-162.0185, thickness=0.923)
lens.add_surface(index=15, radius=629.544, thickness=41.227, material=SiO2)
lens.add_surface(index=16, radius=-226.7397, thickness=0.916)
lens.add_surface(index=17, radius=522.2739, thickness=27.842, material=SiO2)
lens.add_surface(index=18, radius=-582.424, thickness=0.916)
lens.add_surface(index=19, radius=423.729, thickness=22.904, material=SiO2)
lens.add_surface(index=20, radius=-1385.36, thickness=0.916, is_stop=True)
lens.add_surface(index=21, radius=212.039, thickness=33.646, material=SiO2)
lens.add_surface(index=22, radius=802.3695, thickness=55.304)
lens.add_surface(index=23, radius=-776.5697, thickness=8.703, material=SiO2)
lens.add_surface(index=24, radius=106.1728, thickness=24.09)
lens.add_surface(index=25, radius=-200.683, thickness=11.452, material=SiO2)
lens.add_surface(index=26, radius=311.8264, thickness=59.54)
lens.add_surface(index=27, radius=-77.2276, thickness=11.772, material=SiO2)
lens.add_surface(index=28, radius=2317.8032, thickness=11.862)
lens.add_surface(index=29, radius=-290.8859, thickness=22.904, material=SiO2)
lens.add_surface(index=30, radius=-148.3577, thickness=1.373)
lens.add_surface(index=31, radius=-5658.5043, thickness=41.227, material=SiO2)
lens.add_surface(index=32, radius=-151.9858, thickness=0.916)
lens.add_surface(index=33, radius=678.1005, thickness=32.981, material=SiO2)
lens.add_surface(index=34, radius=-358.554, thickness=0.916)
lens.add_surface(index=35, radius=264.2734, thickness=32.814, material=SiO2)
lens.add_surface(index=36, radius=2309.6884, thickness=0.916)
lens.add_surface(index=37, radius=171.2681, thickness=29.015, material=SiO2)
lens.add_surface(index=38, radius=364.7765, thickness=0.918)
lens.add_surface(index=39, radius=113.37, thickness=76.259, material=SiO2)
lens.add_surface(index=40, radius=78.6982, thickness=54.304)
lens.add_surface(index=41, radius=49.5443, thickness=18.65, material=SiO2)
lens.add_surface(index=42, radius=109.8136, thickness=13.07647896)
lens.add_surface(index=43, radius=np.inf)

# Define the aperture (the original NA was 0.15, but we reduce it slightly to avoid
# negative edge thicknesses)
lens.set_aperture(aperture_type="objectNA", value=0.133)

# Define the field
lens.set_field_type(field_type="object_height")
lens.add_field(y=0)
lens.add_field(y=32)
lens.add_field(y=48)

# Define the wavelength
lens.add_wavelength(value=0.248, is_primary=True)

# Specify that the lens is object-space telecentric
lens.obj_space_telecentric = True

# Move last surface to the paraxial image plane
lens.image_solve()

lens.draw(figsize=(12, 3))

lens_mtf = mtf.FFTMTF(lens)
lens_mtf.view(add_reference=True)

problem = optimization.OptimizationProblem()

# Add focal length operand
problem.add_operand(
    operand_type="f2", target=494, weight=1, input_data={"optic": lens}
)

# Add OPD operands for imaging quality
for field in lens.fields.get_field_coords():
    input_data = {
        "optic": lens,
        "Hx": field[0],
        "Hy": field[1],
        "num_rays": 5,
        "wavelength": 0.248,
        "distribution": "gaussian_quad",
    }
    problem.add_operand(
        operand_type="OPD_difference",
        target=0,
        weight=10,
        input_data=input_data,
    )

# Allow the radii of curvature of all surfaces to vary
for k in range(1, lens.surface_group.num_surfaces - 1):
    problem.add_variable(
        lens,
        "radius",
        surface_number=k,
        min_val=-10000,
        max_val=10000,
    )

# Print current merit function value
problem.merit_info()

optimizer = optimization.OptimizerGeneric(problem)
res = optimizer.optimize(tol=1e-9)

problem.merit_info()

lens_mtf = mtf.FFTMTF(lens)
lens_mtf.view(add_reference=True)

opd = wavefront.OPD(lens, field=(0, 1), wavelength=0.248)
opd.view(projection="2d", num_points=256)

print("Zernike Standard Coefficients:")
zernike = wavefront.ZernikeOPD(
    lens,
    (0, 1),
    0.55,
    zernike_type="standard",
    num_terms=21,
)

for k in range(len(zernike.coeffs)):
    print(f"\tZ{k + 1}: {zernike.coeffs[k]:.8f}")

spot = analysis.SpotDiagram(lens)
spot.view()

fan = analysis.RayFan(lens)
fan.view()

lens.info()

Conclusions

  • Starting from a patent design, we optimized a lithographic projection lens to achieve near diffraction-limited performance
  • While significantly improved, the final design still has room for improvement. For example, we could have introduced aspheres on some of the surfaces.

Next tutorials