Tutorial 4a: Tilts, Decenters and Asymmetric Systems

Consolidating tilts and decenters, coordinate breaks, and non-rotationally symmetric systems.

This tutorial shows how to tilt and de-center surfaces in a lens. We will use a spot diagram to show how ray intersection on the image plane are impacted by tilt/decentering.

[1]:
import numpy as np

from optiland import analysis, optic

We start with a simple lens with all surfaces aligned.

[2]:
lens = optic.Optic()

# add surfaces
lens.surfaces.add(index=0, radius=np.inf, thickness=np.inf)
lens.surfaces.add(index=1, thickness=7, radius=19.93, is_stop=True, material="N-SF11")
lens.surfaces.add(index=2, thickness=21.48)
lens.surfaces.add(index=3)

# add aperture
lens.set_aperture(aperture_type="EPD", value=25.4)

# add field
lens.fields.set_type(field_type="angle")
lens.fields.add(y=0)

# add wavelength
lens.wavelengths.add(value=0.587, is_primary=True)

lens.draw(num_rays=10)
[2]:
(<Figure size 1000x400 with 1 Axes>, <Axes: xlabel='Z [mm]', ylabel='Y [mm]'>)
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_5_1.png
[3]:
spot = analysis.SpotDiagram(lens)
spot.view()
[3]:
(<Figure size 1200x400 with 1 Axes>,
 [<Axes: title={'center': 'Hx: 0.000, Hy: 0.000'}, xlabel='X (mm)', ylabel='Y (mm)'>])
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_6_1.png

Now, let’s tilt the first surface by 5 degrees and redraw the lens.

[4]:
lens = optic.Optic()

# add surfaces
lens.surfaces.add(index=0, radius=np.inf, thickness=np.inf)

# == WE ADD THE TILT TO THIS SURFACE ===============
lens.surfaces.add(
    index=1,
    thickness=7,
    radius=19.93,
    is_stop=True,
    material="N-SF11",
    rx=np.radians(5.0),
)
# ==================================================

lens.surfaces.add(index=2, thickness=21.48)
lens.surfaces.add(index=3)

# add aperture
lens.set_aperture(aperture_type="EPD", value=25.4)

# add field
lens.fields.set_type(field_type="angle")
lens.fields.add(y=0)

# add wavelength
lens.wavelengths.add(value=0.587, is_primary=True)

lens.draw(num_rays=10)

spot = analysis.SpotDiagram(lens)
spot.view()
[4]:
(<Figure size 1200x400 with 1 Axes>,
 [<Axes: title={'center': 'Hx: 0.000, Hy: 0.000'}, xlabel='X (mm)', ylabel='Y (mm)'>])
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_8_1.png
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_8_2.png

Let’s decenter the first surface of the lens.

[5]:
lens = optic.Optic()

# add surfaces
lens.surfaces.add(index=0, radius=np.inf, thickness=np.inf)

# == WE DECENTER THIS SURFACE =====================
lens.surfaces.add(
    index=1,
    thickness=7,
    radius=19.93,
    is_stop=True,
    material="N-SF11",
    dy=1.0,  # 1 mm decenter
)
# ==================================================

lens.surfaces.add(index=2, thickness=21.48)

lens.surfaces.add(index=3)

# add aperture
lens.set_aperture(aperture_type="EPD", value=25.4)

# add field
lens.fields.set_type(field_type="angle")
lens.fields.add(y=0)

# add wavelength
lens.wavelengths.add(value=0.587, is_primary=True)

lens.draw(num_rays=10)

spot = analysis.SpotDiagram(lens)
spot.view()
[5]:
(<Figure size 1200x400 with 1 Axes>,
 [<Axes: title={'center': 'Hx: 0.000, Hy: 0.000'}, xlabel='X (mm)', ylabel='Y (mm)'>])
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_10_1.png
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_10_2.png

Part 2: Non Rotationally Symmetric Systems

[1]:
import numpy as np

from optiland import analysis, optic, optimization

1. Surface Coordinate System in Optiland

In rotationally symmetric systems, all the surfaces are typically aligned along the z-axis (so-called optical axis), and passing the thickness parameter to the optic.surfaces.add() method will simply specify the distance between the consecutive surfaces. However, in a non-rotationally symmetric system, the surfaces are not necessarily aligned along the z-axis. To represent this, the surface coordinate system in Optiland includes two additional axes, x and y. The x axis is perpendicular to the optical axis and points in the direction ‘inside the screen’. The y axis is perpendicular to the optical axis and points in the upwards direction.

2. Folded Mirror System Design - Approach 1 (more intuitive)

An overview of the design task can be found in the following image:

diagram

To design this folded mirror system, you need to consider the following:

  • To rotate a surface (say, tilt about the x axis), you need to pass the parameter rx=... in the add_surface() method. The same applies to the tilt about Y or tilt about Z, and their corresponding parameters are ry, and rz, respectively. The default units of rotation of a surface are radians, which means that if you prefer to specify the rotation in degrees, you must use, for example for a 45 degree rotation about X, the parameter rx=np.radians(45).

  • You must not pass thickness after defining an x, y, z position for a previous surface. Hence, if you want to design systems with coordinate breaks, the best approach is to always specify the location of the surfaces using their (x,y,z) coordinates, instead of thickness.

[2]:
lens = optic.Optic()

# System settings
lens.set_aperture(aperture_type="EPD", value=5.0)
lens.fields.set_type(field_type="angle")
lens.fields.add(y=-0.5)
lens.fields.add(y=0)
lens.fields.add(y=0.5)
lens.wavelengths.add(value=0.633, is_primary=True)

# Lens data
lens.surfaces.add(index=0, radius=np.inf, z=-np.inf)
lens.surfaces.add(index=1, z=0)
lens.surfaces.add(index=2, z=10, material="bk7", surface_type="standard")  # material in
lens.surfaces.add(index=3, z=15, material="air", surface_type="standard")  # material out
lens.surfaces.add(
    index=4, z=25, material="mirror", rx=-np.pi / 4, is_stop=True
)  # first mirror
lens.surfaces.add(
    index=5, z=25, y=-15, material="mirror", rx=-np.pi / 4
)  # second mirror
lens.surfaces.add(index=6, z=45, y=-15, material="mirror", rx=np.pi / 4)  # third mirror
lens.surfaces.add(
    index=7, z=45, y=-10, radius=-30, material="bk7", rx=np.pi / 2
)  # material in
lens.surfaces.add(
    index=8, z=45, y=-5, radius=30, material="air", rx=np.pi / 2
)  # material out
lens.surfaces.add(index=9, z=45, y=10, material="mirror", rx=np.pi / 4)  # fourth mirror
lens.surfaces.add(index=10, z=55, y=10)  # Image plane

Note how we define the translation parameters (e.g. x, y, z) and the rotation parameters (e.g. rx, ry, rz) for each surface. Note also, how we must rotate the surfaces of the last focusing lens (surfaces 7 and 8) in order to meet the correct ray propagation direction. In fact, you can try yourself, that if you forget to rotate this surfaces, some of the ray computations will fail, because it will be assumed that the surface is still in the origin coordinate system (i.e. with rx=0)

We can then visualize both the 2D and 3D drawings of the system we have just designed:

[3]:
lens.draw(title="System 2D Layout")
lens.draw3D(num_rays=10)  # Opens in external window when you run the code
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_21_0.png

And we can check its spot diagram (before optimization) as well:

[4]:
spot = analysis.SpotDiagram(lens, num_rings=10, distribution="hexapolar")
spot.view()
[4]:
(<Figure size 1200x400 with 3 Axes>,
 [<Axes: title={'center': 'Hx: 0.000, Hy: -1.000'}, xlabel='X (mm)', ylabel='Y (mm)'>,
  <Axes: title={'center': 'Hx: 0.000, Hy: 0.000'}, xlabel='X (mm)', ylabel='Y (mm)'>,
  <Axes: title={'center': 'Hx: 0.000, Hy: 1.000'}, xlabel='X (mm)', ylabel='Y (mm)'>])
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_23_1.png

2.1 Fine tuning optimization for best focus in the image plane - Simple RMS Spot Optimization

Once the design is finished, we see that the system is still not in the best focus. This means that a simple optimization can be used to optimize the distance from the last surface of the system (i.e. the back surface of the focusing lens) to the image plane (surface index 9).

[5]:
problem = optimization.OptimizationProblem()

# Variable Definition
# thicknesses
problem.add_variable(lens, "thickness", surface_number=9, min_val=10, max_val=20)

# RMS spot size - let's minimize the spot size for each field at the primary wavelength.
# We choose a 'uniform' distribution, so the number of rays actually means the rays
# along one axis.
for field in lens.fields.get_field_coords():
    input_data = {
        "optic": lens,
        "surface_number": -1,
        "Hx": field[0],
        "Hy": field[1],
        "num_rays": 16,
        "wavelength": 0.633,
        "distribution": "uniform",
    }
    problem.add_operand(
        operand_type="rms_spot_size",
        target=0.0,
        weight=10,
        input_data=input_data,
    )

# Local optimizer
optimizer = optimization.OptimizerGeneric(problem)
optimizer.optimize()
problem.info()

# Final spot diagram
spot = analysis.SpotDiagram(lens, num_rings=10, distribution="hexapolar")
spot.view()
╒════╤════════════════════════╤═══════════════════╕
│    │   Merit Function Value │   Improvement (%) │
╞════╪════════════════════════╪═══════════════════╡
│  0 │             0.00643671 │           99.9334 │
╘════╧════════════════════════╧═══════════════════╛
╒════╤════════════════╤══════════╤══════════════╤══════════════╤══════════╤═════════╤═════════╤════════════════╕
│    │ Operand Type   │   Target │ Min. Bound   │ Max. Bound   │   Weight │   Value │   Delta │   Contrib. [%] │
╞════╪════════════════╪══════════╪══════════════╪══════════════╪══════════╪═════════╪═════════╪════════════════╡
│  0 │ rms spot size  │        0 │              │              │       10 │   0.005 │   0.005 │          35.94 │
│  1 │ rms spot size  │        0 │              │              │       10 │   0.004 │   0.004 │          28.13 │
│  2 │ rms spot size  │        0 │              │              │       10 │   0.005 │   0.005 │          35.94 │
╘════╧════════════════╧══════════╧══════════════╧══════════════╧══════════╧═════════╧═════════╧════════════════╛
╒════╤═════════════════╤═══════════╤═════════╤══════════════╤══════════════╕
│    │ Variable Type   │   Surface │   Value │   Min. Bound │   Max. Bound │
╞════╪═════════════════╪═══════════╪═════════╪══════════════╪══════════════╡
│  0 │ thickness       │         9 │ 13.0626 │           10 │           20 │
╘════╧═════════════════╧═══════════╧═════════╧══════════════╧══════════════╛
[5]:
(<Figure size 1200x400 with 3 Axes>,
 [<Axes: title={'center': 'Hx: 0.000, Hy: -1.000'}, xlabel='X (mm)', ylabel='Y (mm)'>,
  <Axes: title={'center': 'Hx: 0.000, Hy: 0.000'}, xlabel='X (mm)', ylabel='Y (mm)'>,
  <Axes: title={'center': 'Hx: 0.000, Hy: 1.000'}, xlabel='X (mm)', ylabel='Y (mm)'>])
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_25_2.png

3. Folded Mirror System Design in Optiland - Approach 2 (less intuitive)

In this approach, we make use of the thickness parameter, though now we define decenters with their corresponding parameters dx, dy (decenters in x, and y, respectively). Comparing this method with the first approach, if we want to rotate the first mirror, we use the same rx parameter as before, but we set its thickness to 0, and in the next added surface, the translation happens in the y-axis (due to the downard-direction reflection at the mirror), and therefore we should locate the second mirror by passing the parameter dy=-15. Because the second mirror changes once again the direction of the rays (they propagate once more in the +z direction), we set the parameter thickness=20 in this second mirror surface, to say that “the distance (z) from this mirror to the next surface is 20 mm”. And so on…

[6]:
lens2 = optic.Optic()

lens2.surfaces.add(index=0, radius=np.inf, thickness=-np.inf)
lens2.surfaces.add(index=1, thickness=10)
lens2.surfaces.add(
    index=2, thickness=5, material="bk7", surface_type="standard"
)  # material in
lens2.surfaces.add(
    index=3, thickness=10, material="air", surface_type="standard"
)  # material out
lens2.surfaces.add(
    index=4, thickness=0, material="mirror", rx=-np.pi / 4, is_stop=True
)  # first mirror
lens2.surfaces.add(
    index=5, thickness=20, dy=-15, material="mirror", rx=-np.pi / 4
)  # second mirror
lens2.surfaces.add(
    index=6, thickness=0, dy=-15, material="mirror", rx=np.pi / 4
)  # third mirror
lens2.surfaces.add(
    index=7, thickness=0, dy=-10, radius=-30, material="bk7", rx=np.pi / 2
)  # material in
lens2.surfaces.add(
    index=8, thickness=0, dy=-5, radius=30, material="air", rx=np.pi / 2
)  # material out
lens2.surfaces.add(
    index=9, thickness=10, dy=10, material="mirror", rx=np.pi / 4
)  # fourth mirror
lens2.surfaces.add(index=10, dy=10)  # Image plane

# System Settings
lens2.set_aperture(aperture_type="EPD", value=5.0)
lens2.fields.set_type(field_type="angle")
lens2.fields.add(y=-0.5)
lens2.fields.add(y=0)
lens2.fields.add(y=0.5)
lens2.wavelengths.add(value=0.633, is_primary=True)

# visulaize the system
lens2.draw()
lens2.draw3D(num_rays=10)
../_images/examples_Tutorial_4a_Tilts_Decenters_and_Asymmetric_Systems_28_0.png

Conclusion: We have explored the surfaces’ coordinate systems in Optiland, as well as providing two approaches to design complex optical systems which involve several changes in coordinates, as it happens with folding mirrors. The first approach presents itself as a more intuitive method, since we are effectively localizing each surface according to its coordinate system in the 3D space. The second approach, however, involves considering that after folding mirrors, the translation happens in another axis and special care is needed to ensure the correct modelling of the system.