Tutorial 6d - Dichroic Mirror Optimization for Polarization Separation

We optimize a thin-film stack to create a dichroic mirror that separates s and p polarizations near 600 nm.

This version demonstrates the operand-centric architecture in ThinFilmOptimizer with a single API entry point: - standard spectral operands with add_operand(…) - a custom user-defined operand registered with register_operand(…) and used through add_operand(…)

[1]:
from optiland.thin_film.optimization import ThinFilmOptimizer
import optiland.backend as be
from optiland.thin_film import ThinFilmStack, SpectralAnalyzer
from optiland.materials import Material, IdealMaterial

SiO2 = Material("SiO2", reference="Gao")
TiO2 = Material("TiO2", reference="Zhukovsky")
BK7 = Material("N-BK7", reference="SCHOTT")
air = IdealMaterial(n=1.0)

dichroic_stack = ThinFilmStack(
    incident_material=air,
    substrate_material=BK7,
    reference_wl_um=0.6,
    reference_AOI_deg=45.0,
)
for i in range(10):  # 10 pairs = 20 layers
    dichroic_stack.add_layer_qwot(material=TiO2, qwot_thickness=1.0, name=f"$TiO_2$")
    dichroic_stack.add_layer_qwot(material=SiO2, qwot_thickness=1.0, name=f"$SiO_2$")
[2]:
optimizer = ThinFilmOptimizer(dichroic_stack)

# Add all thicknesses as optimization variables
for i in range(len(dichroic_stack.layers)):
    optimizer.add_variable(
        layer_index=i,
        min_nm=30,
        max_nm=300
    )

# we want to maximize the polarization contrast (Rs-Rp) at 600 nm and 45° AOI, so we
# minimize the negative contrast, averaged over a wavelength range to make it more
# robust to fabrication variations. The contrast is normalized to be between 0 and 1,
# where 1 corresponds to Rs=1 and Rp=0
wl_nm = be.linspace(595, 605, 11)

# Register and add a custom operand through add_operand
def polarization_contrast(stack: ThinFilmStack, wavelength_nm: be.ndarray, aoi_deg:float):
    """Calculate the polarization contrast (Rs-Rp) averaged over a wavelength range.
    The contrast is normalized to be between 0 and 1, where 1 corresponds to Rs=1 and Rp=0."""
    rs = stack.reflectance_nm_deg(wavelength_nm, aoi_deg, "s")
    rp = stack.reflectance_nm_deg(wavelength_nm, aoi_deg, "p")
    return (1 + be.mean(rs - rp))/2

ThinFilmOptimizer.register_operand(
    "polarization_contrast", polarization_contrast, overwrite=True
)

optimizer.add_operand(
    property="polarization_contrast",
    min_val=0.99,
    input_data={"wavelength_nm": wl_nm, "aoi_deg": 45.0},
    label="Rs-Rp @ 595-605nm, 45deg",
)

# Display optimization information
optimizer.info()
contrast_initial=optimizer.rss()
print(f"Initial RSS: {contrast_initial:.6e}")
ThinFilm Optimizer Information
==================================================
+--------------+---------+
| Property     |   Count |
+==============+=========+
| Stack layers |      20 |
+--------------+---------+
| Variables    |      20 |
+--------------+---------+
| Targets      |       1 |
+--------------+---------+

Variables:
+------+---------+------------------+------------+------------+
|   ID |   Layer |   Thickness (nm) |   Min (nm) |   Max (nm) |
+======+=========+==================+============+============+
|    0 |       0 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    1 |       1 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    2 |       2 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    3 |       3 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    4 |       4 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    5 |       5 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    6 |       6 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    7 |       7 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    8 |       8 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|    9 |       9 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   10 |      10 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   11 |      11 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   12 |      12 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   13 |      13 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   14 |      14 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   15 |      15 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   16 |      16 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   17 |      17 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   18 |      18 |             88.2 |         30 |        300 |
+------+---------+------------------+------------+------------+
|   19 |      19 |            143.6 |         30 |        300 |
+------+---------+------------------+------------+------------+

Targets:
+------+--------------------------+--------+---------+--------------+-------+----------+-------+
|   ID | Prop                     | Type   | Value   | Wavelength   | AOI   |   Weight | Pol   |
+======+==========================+========+=========+==============+=======+==========+=======+
|    0 | Rs-Rp @ 595-605nm, 45deg | custom |         | -            | -     |        1 | -     |
+------+--------------------------+--------+---------+--------------+-------+----------+-------+

Initial RSS: 3.058333e-01
[3]:
# Launch optimization
result = optimizer.optimize(
    method="L-BFGS-B",
    max_iterations=1000,
)

print(f"Merit: {result['initial_merit']:.15f} -> {result['final_merit']:.15f} in {result['iterations']} iterations")
print(f"Improvement: {result['improvement']:.15f}")
Merit: 0.093534002618582 -> 0.000329856133348 in 46 iterations
Improvement: 0.093204146485235
[4]:
from optiland.thin_film.optimization import ThinFilmReport

report = ThinFilmReport(optimizer, result)
report.summary_table()
[4]:
Variable Initial Final Change Unit
0 Layer 0 thickness 88.2 71.4 -16.8 (-19.1%) nm
1 Layer 1 thickness 143.6 145.8 +2.2 (+1.5%) nm
2 Layer 2 thickness 88.2 78.3 -9.9 (-11.2%) nm
3 Layer 3 thickness 143.6 153.6 +10.0 (+7.0%) nm
4 Layer 4 thickness 88.2 199.6 +111.4 (+126.3%) nm
5 Layer 5 thickness 143.6 146.2 +2.6 (+1.8%) nm
6 Layer 6 thickness 88.2 74.2 -14.1 (-16.0%) nm
7 Layer 7 thickness 143.6 135.6 -8.0 (-5.6%) nm
8 Layer 8 thickness 88.2 72.0 -16.2 (-18.4%) nm
9 Layer 9 thickness 143.6 132.5 -11.1 (-7.7%) nm
10 Layer 10 thickness 88.2 70.8 -17.4 (-19.8%) nm
11 Layer 11 thickness 143.6 132.5 -11.1 (-7.7%) nm
12 Layer 12 thickness 88.2 67.3 -20.9 (-23.7%) nm
13 Layer 13 thickness 143.6 161.4 +17.8 (+12.4%) nm
14 Layer 14 thickness 88.2 196.0 +107.7 (+122.1%) nm
15 Layer 15 thickness 143.6 161.7 +18.1 (+12.6%) nm
16 Layer 16 thickness 88.2 77.6 -10.7 (-12.1%) nm
17 Layer 17 thickness 143.6 137.2 -6.4 (-4.5%) nm
18 Layer 18 thickness 88.2 71.3 -16.9 (-19.1%) nm
19 Layer 19 thickness 143.6 140.0 -3.6 (-2.5%) nm
[5]:

import matplotlib.pyplot as plt analyzer = SpectralAnalyzer(dichroic_stack) fig, axes = plt.subplots(1, 3, figsize=(12, 4)) wl_range = be.linspace(500, 700, 201) # Around 600 nm # Calculate before optimization (reset and recalculate) optimizer.reset() Rs_before = dichroic_stack.reflectance_nm_deg(wl_range, 45, 's') Rp_before = dichroic_stack.reflectance_nm_deg(wl_range, 45, 'p') # Restore optimized state for i, var_info in enumerate(optimizer.variables): final_thickness = result['thickness_changes'][i]['final_nm'] / 1000 # nm → μm dichroic_stack.layers[i].update_thickness(final_thickness) Rs_after = dichroic_stack.reflectance_nm_deg(wl_range, 45, 's') Rp_after = dichroic_stack.reflectance_nm_deg(wl_range, 45, 'p') axes[0].plot(wl_range, Rs_before, 'b:', label='$R_s$ before', alpha=0.7) axes[0].plot(wl_range, Rp_before, 'g:', label='$P_p$ before', alpha=0.7) axes[0].plot(wl_range, Rs_after, 'b-', label='$R_s$ after', linewidth=2) axes[0].plot(wl_range, Rp_after, 'g-', label='$P_p$ after', linewidth=2) axes[0].set_xlabel('$\lambda$ (nm)') axes[0].set_ylabel('Reflectance') axes[0].set_title('Spectral Performance (AOI=45°)') axes[0].legend() axes[0].grid(True, alpha=0.3) axes[0].fill_betweenx([0, 1], 595, 605, color='gray', alpha=0.2, label='Optimization Range') # 2. Polarization contrast (Rs - Rp) contrast_before = Rs_before - Rp_before contrast_after = Rs_after - Rp_after axes[1].plot(wl_range, contrast_before, 'g--', label='Before', alpha=0.7) axes[1].plot(wl_range, contrast_after, 'g-', label='After', linewidth=2) axes[1].set_xlabel('$\lambda$ (nm)') axes[1].set_ylabel('Contrast ($R_s - R_p$)') axes[1].set_title('Polarization Contrast') axes[1].legend() axes[1].grid(True, alpha=0.3) # 3. Optimized stack structure dichroic_stack.plot_structure_thickness(axes[2]) axes[2].set_title('Optimized Stack Structure') fig.tight_layout()
<>:25: SyntaxWarning: invalid escape sequence '\l'
<>:38: SyntaxWarning: invalid escape sequence '\l'
<>:25: SyntaxWarning: invalid escape sequence '\l'
<>:38: SyntaxWarning: invalid escape sequence '\l'
C:\Users\kdani\AppData\Local\Temp\ipykernel_3984\3268724171.py:25: SyntaxWarning: invalid escape sequence '\l'
  axes[0].set_xlabel('$\lambda$ (nm)')
C:\Users\kdani\AppData\Local\Temp\ipykernel_3984\3268724171.py:38: SyntaxWarning: invalid escape sequence '\l'
  axes[1].set_xlabel('$\lambda$ (nm)')
../_images/examples_Tutorial_6d_Thin_Film_Optimization_5_1.png