Tutorial 2c - Monte Carlo Raytracing Methods

This tutorial shows one way in which Optiland can be used to run Monte Carlo simulations. Monte Carlo simulations are a computational technique used to model and analyze complex systems that involve randomness or uncertainty. They rely on repeated random sampling to estimate the behavior of a system and provide insights into the range of possible outcomes.

In a Monte Carlo simulation, a large number of random samples are generated based on the input parameters and their associated probability distributions. These samples are then used to simulate the system multiple times, allowing us to observe the distribution of outcomes and calculate various performance metrics.

Here, we will investigate how manufacturing variations of a simple singlet lens can impact performance. Note that this is a common tolerancing task.

Manufacturing variations to consider:

  • Lens radius of curvature

  • Lens index of refraction

Performance metrics to consider:

  • RMS spot size

  • RMS wavefront error

[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from optiland import analysis, materials, optic, wavefront

We choose arbitrary ranges for the lens refractive index and radius:

  • Refractive index: normal distribution, mean = 1.5, std=1e-3

  • Radius of curvature: normal distribution, mean = 100, std=0.5

In total, we will simulate 1000 systems.

[2]:
num_systems = 1000

# set random seed for repeatability
np.random.seed(42)

refractive_index = np.random.normal(loc=1.5, scale=1e-3, size=num_systems)
radius_of_curvature = np.random.normal(loc=100, scale=0.5, size=num_systems)

Let’s define a helper class to easily generate random lenses. This is based on the simple singlet in optiland/samples/simple.py.

[3]:
class SingletConfigurable(optic.Optic):
    """A configurable plano-convex singlet"""

    def __init__(self, radius_of_curvature, refractive_index):
        super().__init__()

        # define the material for the singlet
        ideal_material = materials.IdealMaterial(n=refractive_index, k=0)

        # add surfaces
        self.surfaces.add(index=0, radius=np.inf, thickness=np.inf)
        self.surfaces.add(
            index=1,
            thickness=5,
            radius=radius_of_curvature,
            is_stop=True,
            material=ideal_material,
        )
        self.surfaces.add(index=2, thickness=196.667)
        self.surfaces.add(index=3)

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

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

        # add wavelength
        self.wavelengths.add(value=0.55, is_primary=True)

Let’s view the first random system.

[4]:
lens = SingletConfigurable(radius_of_curvature[0], refractive_index[0])
lens.draw(num_rays=5)
[4]:
(<Figure size 1000x400 with 1 Axes>, <Axes: xlabel='Z [mm]', ylabel='Y [mm]'>)
../_images/examples_Tutorial_2c_Monte_Carlo_Raytracing_9_1.png

Now we will run the Monte Carlo simulation. In each of the 1000 iterations, we generate a random system, compute the rms spot radius and rms wavefront error, and record these values for later analysis.

This code uses functionalities from the analysis and wavefront modules, which will be discussed in more detail in future tutorials.

[5]:
rms_spot_radius = []
rms_wavefront_error = []

for k in range(num_systems):
    # generate a random singlet
    lens = SingletConfigurable(radius_of_curvature[k], refractive_index[k])

    # get value of rms spot radius at field index = 0 and wavelength index = 0
    spot = analysis.SpotDiagram(lens)
    value = spot.rms_spot_radius()[0][0]
    rms_spot_radius.append(value)

    # get rms wavefront error
    opd = wavefront.OPD(lens, field=(0, 0), wavelength=0.55)
    rms_wavefront_error.append(opd.rms())

Let’s plot the output distributions of our computed metrics:

[6]:
plt.hist(rms_spot_radius, color="C0", edgecolor="k", alpha=0.8)
plt.xlabel("RMS Spot Radius (mm)")
plt.ylabel("Occurrences")
plt.show()
../_images/examples_Tutorial_2c_Monte_Carlo_Raytracing_13_0.png
[7]:
plt.hist(rms_wavefront_error, color="C1", edgecolor="k", alpha=0.8)
plt.xlabel("RMS Wavefront Error (waves at 0.55 µm)")
plt.ylabel("Occurrences")
plt.show()
../_images/examples_Tutorial_2c_Monte_Carlo_Raytracing_14_0.png

Let’s dive deeper into the relationships by looking at the correlation matrix

[8]:
import seaborn as sns  # pip install seaborn
[9]:
df = pd.DataFrame(
    dict(
        radius_of_curvature=radius_of_curvature,
        refractive_index=refractive_index,
        rms_spot_radius=rms_spot_radius,
        rms_wavefront_error=rms_wavefront_error,
    ),
)
[10]:
# This cell is based on https://seaborn.pydata.org/examples/many_pairwise_correlations.html

# Compute the correlation matrix
corr = df.corr()

# Set up the matplotlib figure
f, ax = plt.subplots(figsize=(5, 3))

# Generate a custom diverging colormap
cmap = sns.diverging_palette(230, 20, as_cmap=True)

# Draw the heatmap with the mask and correct aspect ratio
sns.heatmap(corr, cmap=cmap, vmax=0.3, center=0, square=True, linewidths=0.5)

plt.show()
../_images/examples_Tutorial_2c_Monte_Carlo_Raytracing_18_0.png

There are several relationships we can identify:

  • The RMS spot size positively correlates to the RMS wavefront error, as expected.

  • There is no correlation between refractive index and radius of curvature, as expected.

  • There is a strong negatively correlation between the RMS spot radius and the radius of curvature.

Note that these relationships are not universally true, but are based on the Monte Carlo input distributions used, the system tested, etc. With different inputs, or a different optical system, the results would undoubtedly differ.