Thin Films

Anti-Reflective Coating

Design broadband AR coatings to eliminate ghost reflections.

BeginnerCoatings & PolarizationNumPy backend8 min read

Introduction

Designing a Broadband Anti-Reflective (BBAR) coating to minimize reflection from a glass surface.

Uncoated glass reflects about 4% of incident light per surface due to the refractive index mismatch between air (n1n \approx 1) and glass (n1.5n \approx 1.5). In multi-element optical systems, these losses accumulate and cause ghost images.

This tutorial demonstrates:

  1. Baseline Analysis: The reflection of a bare glass surface (Fresnel reflection).
  2. BBAR Design: Creating a 4-layer Anti-Reflective coating using MgF2MgF_2 and TiO2TiO_2.
  3. Performance Comparison: Visually and numerically comparing the coated vs. uncoated surface.
  4. Angular Stability: Evaluating performance at oblique angles of incidence (0,30,600^\circ, 30^\circ, 60^\circ).

Core concepts used

stack.add_layer_nm(material, thickness_nm)
Explicitly sets the layer's physical thickness in nanometers, typical for finalized designs.
Reflectance %
A measure of how much light is lost at each interface. Lower is better for imaging systems.
BBAR Design
Broadband AR coatings use layered materials to cancel out reflections across the entire visible spectrum (400 to 700 nm).
set_custom_coating(stack)
Updates a lens surface to use your newly designed multilayer dielectric stack.

Step-by-step build

1

Import libraries

python
import numpy as np
import matplotlib.pyplot as plt
from optiland import optic
from optiland.materials import Material, IdealMaterial
from optiland.thin_film import ThinFilmStack
from optiland.coatings import ThinFilmCoating, FresnelCoating
2

Define materials and establish the uncoated Fresnel baseline

We define the materials and the reference wavelength (λ=550\lambda = 550 nm).

python
# Define Materials
air = IdealMaterial(n=1.0)
glass = Material("N-BK7", reference="SCHOTT")
mgf2 = Material("MgF2", reference="Li")    # Low Index (~1.38)
tio2 = Material("TiO2", reference="Siefke") # High Index (~2.5)

# Setup wavelength range (Visible Spectrum)
wavelengths = np.linspace(0.45, 0.7, 301) # 450-700 nm in µm

# Baseline: Uncoated Glass (Fresnel Reflection)
# We use FresnelCoating to simulate the bare interface
bare_surface = FresnelCoating(air, glass)
3

Design the 4-layer BBAR stack

We implement a 4-layer BBAR design (LHLH structure). Crucially, the first layer facing air must be the Low Index material to avoid large reflections.

Stack Recipe (Air \rightarrow Glass):

  1. MgF2 (94 nm) - Low Index (Outer Layer)
  2. TiO2 (117 nm) - High Index
  3. MgF2 (38 nm) - Low Index (Thin)
  4. TiO2 (14 nm) - High Index (Inner Matching Layer)

This structure is optimized to suppress reflection across the visible spectrum (400-700nm).

python
# Create the Stack
bbar_stack = ThinFilmStack(incident_material=air, substrate_material=glass)

# Add layers (incident -> substrate)
# Start with Low Index (MgF2) facing Air to minimize first surface reflection.
# Thicknesses (um): ~94nm, ~117nm, ~38nm, ~14nm
# This is an optimized structure for 400-700nm.
bbar_stack.add_layer(mgf2, 0.094, "L1 (Outer)")
bbar_stack.add_layer(tio2, 0.117, "H1")
bbar_stack.add_layer(mgf2, 0.038, "L2")
bbar_stack.add_layer(tio2, 0.014, "H2 (Inner)")

# Create the Coating Object
bbar_coating = ThinFilmCoating(air, glass, bbar_stack)

# Inspect the design
print(bbar_stack)
4

Compare BBAR reflectance against the bare-glass baseline

We compare the Reflectance (RR) of the BBAR coating against the bare glass baseline at normal incidence (00^\circ).

python
# Compute Reflectance for BBAR
R_bbar = bbar_stack.compute_rtRTA(wavelengths, aoi_rad=0.0, polarization="u")["R"]

# Compute Reflectance for Bare Glass
# We calculate theoretical Fresnel reflection for comparison
n_glass = glass.n(wavelengths)
R_glass = ((1.0 - n_glass) / (1.0 + n_glass))**2

# Visualization
plt.figure(figsize=(10, 6))
plt.plot(wavelengths * 1000, R_glass * 100, 'k--',
      linewidth=1.5, label="Bare Glass (~4.2%)")
plt.plot(wavelengths * 1000, R_bbar * 100, 'b-', linewidth=2.5, label="4-Layer BBAR")

plt.title("Broadband AR Coating Performance")
plt.xlabel("Wavelength (nm)")
plt.ylabel("Reflectance (%)")
plt.grid(True, alpha=0.3)
plt.legend()
plt.ylim(0, 5)
plt.show()

avg_R_bbar = np.mean(R_bbar) * 100
print(f"Average Reflectance (400-700nm): {avg_R_bbar:.2f}%")
3. Spectral Performance Analysis
5

Trace rays through coated and uncoated doublet systems and compare transmission

In this section, we compare the total transmission of the lens system coated with our custom BBAR stack versus a standard Fresnel (uncoated) system. We will visualize the pupil transmission map for both cases.

python
from optiland.rays import PolarizationState

class CoatedDoublet(optic.Optic):
 def __init__(self, coating=None):
     super().__init__()

     self.add_surface(index=0, radius=np.inf, thickness=np.inf)
     self.add_surface(
         index=1,
         radius=29.32908,
         thickness=0.7,
         material="N-BK7",
         is_stop=True,
         coating=coating,
     )
     self.add_surface(index=2, radius=-20.06842, thickness=0.032, coating=coating)
     self.add_surface(
         index=3,
         radius=-20.08770,
         thickness=0.5780,
         material=("SF2", "schott"),
         coating=coating,
     )
     self.add_surface(index=4, radius=-66.54774, thickness=47.3562, coating=coating)
     self.add_surface(index=5)

     self.set_aperture(aperture_type="imageFNO", value=8.0)

     self.set_field_type(field_type="angle")
     self.add_field(y=0.0)
     self.add_field(y=0.7)
     self.add_field(y=1.0)

     self.add_wavelength(value=0.4861)
     self.add_wavelength(value=0.5876, is_primary=True)
     self.add_wavelength(value=0.6563)

     self.update_paraxial()
     self.image_solve()

     # Set Polarization (Required for ThinFilmCoating)
     state = PolarizationState(is_polarized=False)
     self.set_polarization(state)

# 1. System with BBAR Coating
lens_bbar = CoatedDoublet(coating=bbar_coating)
rays_bbar = lens_bbar.trace(
 Hx=0, Hy=0, wavelength=0.55, num_rays=256, distribution="uniform"
)
intensity_bbar = rays_bbar.i

# 2. System with Standard Fresnel Surface (Air/Glass Refraction)
# FresnelCoating simulates the standard reflection loss at each interface (~4%)
fresnel_coating = FresnelCoating(air, glass)
lens_fresnel = CoatedDoublet(coating=fresnel_coating)
rays_fresnel = lens_fresnel.trace(
 Hx=0, Hy=0, wavelength=0.55, num_rays=256, distribution="uniform"
)
intensity_fresnel = rays_fresnel.i

print(f"Average Transmission (BBAR Coated): {np.mean(intensity_bbar):.4f}")
print(f"Average Transmission (Fresnel Uncoated): {np.mean(intensity_fresnel):.4f}")
improvement = (np.mean(intensity_bbar) - np.mean(intensity_fresnel))
improvement /= np.mean(intensity_fresnel)
print(f"Improvement: {improvement * 100:.1f}%")
6

Visualize the pupil transmission map for BBAR vs Fresnel

python
# Get pupil coordinates (Stop is at surface 1)
x_stop_bbar = lens_bbar.surface_group.x[1, :]
y_stop_bbar = lens_bbar.surface_group.y[1, :]

x_stop_fresnel = lens_fresnel.surface_group.x[1, :]
y_stop_fresnel = lens_fresnel.surface_group.y[1, :]

# Comparative visualization
fig, axes = plt.subplots(1, 2, figsize=(14, 6), sharex=True, sharey=True)

# Plot BBAR coated (own scale + own colorbar)
sc1 = axes[0].scatter(
 x_stop_bbar,
 y_stop_bbar,
 c=intensity_bbar,
 s=20,
 cmap="plasma",
 vmin=np.min(intensity_bbar),
 vmax=np.max(intensity_bbar),
)
axes[0].set_title(f"BBAR Coated (Avg T: {np.mean(intensity_bbar):.3f})")
axes[0].set_aspect("equal", adjustable="box")
axes[0].set_xlabel("X (mm)")
axes[0].set_ylabel("Y (mm)")
cbar1 = fig.colorbar(sc1, ax=axes[0], orientation="vertical", fraction=0.046, pad=0.04)
cbar1.set_label("Transmission (BBAR)")

# Plot Fresnel uncoated (own scale + own colorbar)
sc2 = axes[1].scatter(
 x_stop_fresnel,
 y_stop_fresnel,
 c=intensity_fresnel,
 s=20,
 cmap="plasma",
 vmin=np.min(intensity_fresnel),
 vmax=np.max(intensity_fresnel),
)
axes[1].set_title(f"Fresnel Uncoated (Avg T: {np.mean(intensity_fresnel):.3f})")
axes[1].set_aspect("equal", adjustable="box")
axes[1].set_xlabel("X (mm)")
cbar2 = fig.colorbar(sc2, ax=axes[1], orientation="vertical", fraction=0.046, pad=0.04)
cbar2.set_label("Transmission (Fresnel)")

plt.suptitle("Pupil Transmission Map Comparison", fontsize=14)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
Step
Show full code listing
python
import numpy as np
import matplotlib.pyplot as plt
from optiland import optic
from optiland.materials import Material, IdealMaterial
from optiland.thin_film import ThinFilmStack
from optiland.coatings import ThinFilmCoating, FresnelCoating

# Define Materials
air = IdealMaterial(n=1.0)
glass = Material("N-BK7", reference="SCHOTT")
mgf2 = Material("MgF2", reference="Li")    # Low Index (~1.38)
tio2 = Material("TiO2", reference="Siefke") # High Index (~2.5)

# Setup wavelength range (Visible Spectrum)
wavelengths = np.linspace(0.45, 0.7, 301) # 450-700 nm in µm

# Baseline: Uncoated Glass (Fresnel Reflection)
# We use FresnelCoating to simulate the bare interface
bare_surface = FresnelCoating(air, glass)

# Create the Stack
bbar_stack = ThinFilmStack(incident_material=air, substrate_material=glass)

# Add layers (incident -> substrate)
# Start with Low Index (MgF2) facing Air to minimize first surface reflection.
# Thicknesses (um): ~94nm, ~117nm, ~38nm, ~14nm
# This is an optimized structure for 400-700nm.
bbar_stack.add_layer(mgf2, 0.094, "L1 (Outer)")
bbar_stack.add_layer(tio2, 0.117, "H1")
bbar_stack.add_layer(mgf2, 0.038, "L2")
bbar_stack.add_layer(tio2, 0.014, "H2 (Inner)")

# Create the Coating Object
bbar_coating = ThinFilmCoating(air, glass, bbar_stack)

# Inspect the design
print(bbar_stack)

# Compute Reflectance for BBAR
R_bbar = bbar_stack.compute_rtRTA(wavelengths, aoi_rad=0.0, polarization="u")["R"]

# Compute Reflectance for Bare Glass
# We calculate theoretical Fresnel reflection for comparison
n_glass = glass.n(wavelengths)
R_glass = ((1.0 - n_glass) / (1.0 + n_glass))**2

# Visualization
plt.figure(figsize=(10, 6))
plt.plot(wavelengths * 1000, R_glass * 100, 'k--',
       linewidth=1.5, label="Bare Glass (~4.2%)")
plt.plot(wavelengths * 1000, R_bbar * 100, 'b-', linewidth=2.5, label="4-Layer BBAR")

plt.title("Broadband AR Coating Performance")
plt.xlabel("Wavelength (nm)")
plt.ylabel("Reflectance (%)")
plt.grid(True, alpha=0.3)
plt.legend()
plt.ylim(0, 5)
plt.show()

avg_R_bbar = np.mean(R_bbar) * 100
print(f"Average Reflectance (400-700nm): {avg_R_bbar:.2f}%")

from optiland.rays import PolarizationState

class CoatedDoublet(optic.Optic):
  def __init__(self, coating=None):
      super().__init__()

      self.add_surface(index=0, radius=np.inf, thickness=np.inf)
      self.add_surface(
          index=1,
          radius=29.32908,
          thickness=0.7,
          material="N-BK7",
          is_stop=True,
          coating=coating,
      )
      self.add_surface(index=2, radius=-20.06842, thickness=0.032, coating=coating)
      self.add_surface(
          index=3,
          radius=-20.08770,
          thickness=0.5780,
          material=("SF2", "schott"),
          coating=coating,
      )
      self.add_surface(index=4, radius=-66.54774, thickness=47.3562, coating=coating)
      self.add_surface(index=5)

      self.set_aperture(aperture_type="imageFNO", value=8.0)

      self.set_field_type(field_type="angle")
      self.add_field(y=0.0)
      self.add_field(y=0.7)
      self.add_field(y=1.0)

      self.add_wavelength(value=0.4861)
      self.add_wavelength(value=0.5876, is_primary=True)
      self.add_wavelength(value=0.6563)

      self.update_paraxial()
      self.image_solve()

      # Set Polarization (Required for ThinFilmCoating)
      state = PolarizationState(is_polarized=False)
      self.set_polarization(state)

# 1. System with BBAR Coating
lens_bbar = CoatedDoublet(coating=bbar_coating)
rays_bbar = lens_bbar.trace(
  Hx=0, Hy=0, wavelength=0.55, num_rays=256, distribution="uniform"
)
intensity_bbar = rays_bbar.i

# 2. System with Standard Fresnel Surface (Air/Glass Refraction)
# FresnelCoating simulates the standard reflection loss at each interface (~4%)
fresnel_coating = FresnelCoating(air, glass)
lens_fresnel = CoatedDoublet(coating=fresnel_coating)
rays_fresnel = lens_fresnel.trace(
  Hx=0, Hy=0, wavelength=0.55, num_rays=256, distribution="uniform"
)
intensity_fresnel = rays_fresnel.i

print(f"Average Transmission (BBAR Coated): {np.mean(intensity_bbar):.4f}")
print(f"Average Transmission (Fresnel Uncoated): {np.mean(intensity_fresnel):.4f}")
improvement = (np.mean(intensity_bbar) - np.mean(intensity_fresnel))
improvement /= np.mean(intensity_fresnel)
print(f"Improvement: {improvement * 100:.1f}%")

# Get pupil coordinates (Stop is at surface 1)
x_stop_bbar = lens_bbar.surface_group.x[1, :]
y_stop_bbar = lens_bbar.surface_group.y[1, :]

x_stop_fresnel = lens_fresnel.surface_group.x[1, :]
y_stop_fresnel = lens_fresnel.surface_group.y[1, :]

# Comparative visualization
fig, axes = plt.subplots(1, 2, figsize=(14, 6), sharex=True, sharey=True)

# Plot BBAR coated (own scale + own colorbar)
sc1 = axes[0].scatter(
  x_stop_bbar,
  y_stop_bbar,
  c=intensity_bbar,
  s=20,
  cmap="plasma",
  vmin=np.min(intensity_bbar),
  vmax=np.max(intensity_bbar),
)
axes[0].set_title(f"BBAR Coated (Avg T: {np.mean(intensity_bbar):.3f})")
axes[0].set_aspect("equal", adjustable="box")
axes[0].set_xlabel("X (mm)")
axes[0].set_ylabel("Y (mm)")
cbar1 = fig.colorbar(sc1, ax=axes[0], orientation="vertical", fraction=0.046, pad=0.04)
cbar1.set_label("Transmission (BBAR)")

# Plot Fresnel uncoated (own scale + own colorbar)
sc2 = axes[1].scatter(
  x_stop_fresnel,
  y_stop_fresnel,
  c=intensity_fresnel,
  s=20,
  cmap="plasma",
  vmin=np.min(intensity_fresnel),
  vmax=np.max(intensity_fresnel),
)
axes[1].set_title(f"Fresnel Uncoated (Avg T: {np.mean(intensity_fresnel):.3f})")
axes[1].set_aspect("equal", adjustable="box")
axes[1].set_xlabel("X (mm)")
cbar2 = fig.colorbar(sc2, ax=axes[1], orientation="vertical", fraction=0.046, pad=0.04)
cbar2.set_label("Transmission (Fresnel)")

plt.suptitle("Pupil Transmission Map Comparison", fontsize=14)
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()

Conclusions

  • A 4-layer MgF2/TiO2 BBAR stack (LHLH structure) reduces average reflectance from ~4.2% per surface to well below 1% across 450–700 nm, demonstrating how destructive interference in a multilayer dielectric can dramatically suppress Fresnel losses.
  • Starting the stack with a low-index MgF2 layer facing air is a key design rule: it minimizes the index discontinuity at the first interface and sets up the impedance-matching conditions needed for broadband suppression.
  • Applying the BBAR coating to every surface of a glass doublet via set_custom_coating shows the system-level impact — ray-traced pupil transmission maps confirm a measurable throughput improvement relative to uncoated Fresnel surfaces.
  • The FresnelCoating class provides a convenient reference baseline, enabling direct numerical and visual comparison of coated vs. uncoated performance within the same optical system model.
  • The workflow — define materials, build the stack, compute reflectance with compute_rtRTA, apply to a lens model, and trace rays — is a complete end-to-end pipeline for evaluating thin-film coatings at the system level.

Next tutorials

Original notebook: Tutorial_6f_AR_Coating_System.ipynb on GitHub · ReadTheDocs