Anti-Reflective Coating
Design broadband AR coatings to eliminate ghost reflections.
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 () and glass (). In multi-element optical systems, these losses accumulate and cause ghost images.
This tutorial demonstrates:
- Baseline Analysis: The reflection of a bare glass surface (Fresnel reflection).
- BBAR Design: Creating a 4-layer Anti-Reflective coating using and .
- Performance Comparison: Visually and numerically comparing the coated vs. uncoated surface.
- Angular Stability: Evaluating performance at oblique angles of incidence ().
Core concepts used
Step-by-step build
Import libraries
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, FresnelCoatingDefine materials and establish the uncoated Fresnel baseline
We define the materials and the reference wavelength ( nm).
# 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)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 Glass):
- MgF2 (94 nm) - Low Index (Outer Layer)
- TiO2 (117 nm) - High Index
- MgF2 (38 nm) - Low Index (Thin)
- TiO2 (14 nm) - High Index (Inner Matching Layer)
This structure is optimized to suppress reflection across the visible spectrum (400-700nm).
# 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)Compare BBAR reflectance against the bare-glass baseline
We compare the Reflectance () of the BBAR coating against the bare glass baseline at normal incidence ().
# 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}%")
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.
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}%")Visualize the pupil transmission map for BBAR vs Fresnel
# 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()
Show full code listing
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_coatingshows the system-level impact — ray-traced pupil transmission maps confirm a measurable throughput improvement relative to uncoated Fresnel surfaces. - The
FresnelCoatingclass 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