Needle Synthesis for Thin Film Design
Automatically discover optimal thin-film stack configurations using the needle synthesis algorithm.
Introduction
This tutorial demonstrates the needle synthesis algorithm for discovering optimal thin film stack configurations, using real dispersive materials from the optiland catalog on N-BK7 glass.
Two design examples of increasing complexity:
- Part 1 — Broadband AR coating: R < 1% across 420–680 nm, starting from a single MgF2 layer
- Part 2 — Dichroic beamsplitter: R > 95% for 420–540 nm, T > 95% for 560–680 nm, starting from a (HL)³ quarter-wave stack
Reference: Tikhonravov & Trubetskov, Development of the needle optimization technique and new features of OptiLayer design software, SPIE Vol. 2253, 1994.
Core concepts used
Step-by-step build
Import Required Modules
import numpy as np
import matplotlib.pyplot as plt
from optiland.materials import Material, IdealMaterial
from optiland.thin_film import ThinFilmStack, SpectralAnalyzer
from optiland.thin_film.optimization import NeedleSynthesis
from optiland.thin_film.optimization.operand.thin_film import ThinFilmOperandPart 1: Broadband AR Coating (R < 1%)
Define materials and starting design
We use real dispersive materials from the optiland catalog:
| Material | Role | n @ 550 nm | Reference |
|---|---|---|---|
| N-BK7 | Substrate | 1.519 | Schott |
| SiO2 | Low-index layer | 1.460 | Malitson |
| TiO2 | High-index layer | 2.648 | Devore |
| MgF2 | Very low-index layer | 1.379 | Dodge |
| Al2O3 | Mid-index layer | 1.770 | Malitson |
We start from a single MgF2 quarter-wave layer — the simplest possible AR coating. A single low-index layer on N-BK7 gives roughly 1.4% reflectance, which already violates our < 1% spec at the band edges.
# Incident medium
air = IdealMaterial(n=1.0)
# Substrate
nbk7 = Material("N-BK7")
# Candidate coating materials (dispersive)
sio2 = Material("SiO2", reference="Malitson")
tio2 = Material("TiO2", reference="Devore-o")
mgf2 = Material("MgF2", reference="Dodge-o")
al2o3 = Material("Al2O3", reference="Malitson")
# Print refractive indices at 550 nm
for name, mat in [("N-BK7", nbk7), ("SiO2", sio2), ("TiO2", tio2),
("MgF2", mgf2), ("Al2O3", al2o3)]:
print(f"{name:6s} n(550nm) = {np.asarray(mat.n(0.55)).item():.4f}")
# Starting design: single MgF2 layer
stack = ThinFilmStack(incident_material=air, substrate_material=nbk7)
stack.add_layer_nm(mgf2, 100.0, name="MgF2")
print("\nStarting design:")
print(stack)Starting performance
The single MgF2 layer gives ~1.4% average reflectance. The 1% spec line (dashed red) shows the design fails across most of the band.
wl_plot = np.linspace(400, 720, 300)
R_start = np.array([ThinFilmOperand.reflectance(stack, wl) for wl in wl_plot])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_plot, R_start * 100, "-", color="gray",
linewidth=2, label="Single MgF2 layer")
ax.axhline(1.0, color="red", linestyle="--",
linewidth=1, alpha=0.7, label="Spec: R < 1%")
ax.axvspan(420, 680, alpha=0.06, color="blue", label="Target band (420–680 nm)")
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Starting Design — Single MgF2 Layer on N-BK7")
ax.set_ylim(0, 5)
ax.legend(loc="upper right")
ax.grid(True, alpha=0.3)
fig.tight_layout();
Configure needle synthesis
The algorithm will:
- Screen trial needle insertions at sampled positions within every layer
- Try all four candidate materials at each position
- Insert the needle that reduces the merit function the most
- Re-optimize all layer thicknesses after each insertion
- Repeat until no further improvement is found
We target R = 0 at 30 wavelengths across 420–680 nm, driving the optimizer to push reflectance as low as possible everywhere.
ns = NeedleSynthesis(
stack=stack,
candidate_materials=[sio2, tio2, mgf2, al2o3],
needle_thickness_nm=1.0,
min_thickness_nm=2.0,
max_iterations=12,
num_positions_per_layer=8,
optimizer_max_iter=200,
)
# Dense broadband target: minimize R across 420-680 nm
wavelengths = np.linspace(420, 680, 30).tolist()
ns.add_spectral_target("R", wavelengths, "equal", 0.0)Run needle synthesis
The verbose output shows each iteration: which material was inserted, the needle thickness, and how the merit function evolves.
result = ns.run(verbose=True)Results summary
print(f"Success: {result.success}")
print(f"Iterations: {result.num_iterations}")
print(f"Layers added: {result.num_layers_added}")
print(f"Initial merit: {result.initial_merit:.6e}")
print(f"Final merit: {result.final_merit:.6e}")
print(f"Improvement: {(1 - result.final_merit / result.initial_merit) * 100:.1f}%")
print(f"\nFinal stack:")
print(result.stack)
# Check against spec: R < 1% everywhere in 420-680 nm
wl_check = np.linspace(420, 680, 100)
R_vals = np.array([ThinFilmOperand.reflectance(result.stack, wl) for wl in wl_check])
print(f"\nAverage R (420-680 nm): {np.mean(R_vals)*100:.3f}%")
print(f"Peak R (420-680 nm): {np.max(R_vals)*100:.3f}%")
print(f"R < 1% across full band: {np.all(R_vals < 0.01)}")Merit function convergence
Each accepted needle insertion reduces the merit function. The rollback mechanism ensures rejected insertions (those that worsen performance after re-optimization) are never kept.
merits = [result.initial_merit] + [h.merit_after for h in result.history]
materials_used = [h.material_name for h in result.history]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(merits)), merits, "o-", color="steelblue", linewidth=2, markersize=8)
# Annotate each insertion with material name
for i, (m, name) in enumerate(zip(merits[1:], materials_used), 1):
ax.annotate(name, (i, m), textcoords="offset points",
xytext=(5, 10), fontsize=9, color="darkred")
ax.set_xlabel("Accepted Iteration")
ax.set_ylabel("Merit Function")
ax.set_title("Needle Synthesis Convergence")
ax.set_yscale("log")
ax.grid(True, alpha=0.3)
fig.tight_layout();
Final stack structure
The algorithm autonomously selected which materials to use and determined the optimal layer thicknesses.
fig, ax = result.stack.plot_structure()
Before vs. after comparison
The final design meets the stringent R < 1% spec across the entire 420–680 nm band — something impossible with a single-layer coating on N-BK7.
# Rebuild starting design for comparison
stack_initial = ThinFilmStack(incident_material=air, substrate_material=nbk7)
stack_initial.add_layer_nm(mgf2, 100.0, name="MgF2")
wl_eval = np.linspace(400, 720, 300)
R_initial = np.array([
ThinFilmOperand.reflectance(stack_initial, wl) for wl in wl_eval
])
R_final = np.array([ThinFilmOperand.reflectance(result.stack, wl) for wl in wl_eval])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_eval, R_initial * 100, "--", color="gray",
linewidth=1.5, label="Starting design (1 layer)")
ax.plot(wl_eval, R_final * 100, "-", color="steelblue",
linewidth=2,
label=f"After needle synthesis ({len(result.stack.layers)} layers)")
ax.axhline(1.0, color="red", linestyle="--",
linewidth=1, alpha=0.7, label="Spec: R < 1%")
ax.axvspan(420, 680, alpha=0.06, color="blue")
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Broadband AR on N-BK7 — Needle Synthesis Result")
ax.set_ylim(0, 5)
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout();
Part 2: Dichroic Beamsplitter
Starting design: (HL)³ quarter-wave stack
We start from a simple 6-layer TiO2/SiO2 quarter-wave stack tuned to 500 nm. This gives some initial reflectance in the blue but is far from meeting spec.
# (HL)^3 quarter-wave stack at 500 nm
# QW optical thickness: n*d = 500/4 = 125 nm
# TiO2 (n~2.65): d = 125/2.65 ~ 47 nm
# SiO2 (n~1.46): d = 125/1.46 ~ 86 nm
stack_d = ThinFilmStack(incident_material=air, substrate_material=nbk7)
for _ in range(3):
stack_d.add_layer_nm(tio2, 47.0, name="TiO2")
stack_d.add_layer_nm(sio2, 86.0, name="SiO2")
print(stack_d)
# Plot starting performance
wl_plot = np.linspace(400, 720, 300)
R_start_d = np.array([ThinFilmOperand.reflectance(stack_d, wl) for wl in wl_plot])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_plot, R_start_d * 100, "-", color="gray", linewidth=2)
ax.axvspan(420, 540, alpha=0.08, color="blue", label="Reflect: R > 95%")
ax.axvspan(560, 680, alpha=0.08, color="red", label="Transmit: T > 95%")
ax.axhline(95, color="blue", linestyle="--", linewidth=1, alpha=0.5)
ax.axhline(5, color="red", linestyle="--", linewidth=1, alpha=0.5)
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Starting Design — (HL)³ Quarter-Wave Stack on N-BK7")
ax.set_ylim(0, 100)
ax.legend(loc="center right")
ax.grid(True, alpha=0.3)
fig.tight_layout();
Run needle synthesis for the dichroic
ns_d = NeedleSynthesis(
stack=stack_d,
candidate_materials=[sio2, tio2, mgf2, al2o3],
needle_thickness_nm=1.0,
min_thickness_nm=2.0,
max_iterations=8,
num_positions_per_layer=3,
optimizer_max_iter=80,
)
# Reflection band: R → 1 for 420-540 nm
wl_reflect = np.linspace(420, 540, 10).tolist()
ns_d.add_spectral_target("R", wl_reflect, "equal", 1.0)
# Transmission band: R → 0 for 560-680 nm
wl_transmit = np.linspace(560, 680, 10).tolist()
ns_d.add_spectral_target("R", wl_transmit, "equal", 0.0)
result_d = ns_d.run(verbose=True)Dichroic results summary
print(f"Success: {result_d.success}")
print(f"Layers added: {result_d.num_layers_added}")
print(f"Initial merit: {result_d.initial_merit:.6e}")
print(f"Final merit: {result_d.final_merit:.6e}")
improvement_pct = (1 - result_d.final_merit / result_d.initial_merit) * 100
print(f"Improvement: {improvement_pct:.1f}%")
print(f"\nFinal stack ({len(result_d.stack.layers)} layers):")
print(result_d.stack)
# Check against specs
wl_r = np.linspace(420, 540, 50)
wl_t = np.linspace(560, 680, 50)
R_reflect = np.array([ThinFilmOperand.reflectance(result_d.stack, wl) for wl in wl_r])
R_transmit = np.array([ThinFilmOperand.reflectance(result_d.stack, wl) for wl in wl_t])
print(f"\nReflection band (420-540 nm):")
print(f" Average R: {np.mean(R_reflect)*100:.1f}%")
print(f" Min R: {np.min(R_reflect)*100:.1f}%")
print(f"\nTransmission band (560-680 nm):")
print(f" Average T: {(1-np.mean(R_transmit))*100:.1f}%")
print(f" Min T: {(1-np.max(R_transmit))*100:.1f}%")Dichroic convergence analysis
merits_d = [result_d.initial_merit] + [h.merit_after for h in result_d.history]
materials_d = [h.material_name for h in result_d.history]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(
range(len(merits_d)), merits_d,
"o-", color="steelblue", linewidth=2, markersize=8
)
for i, (m, name) in enumerate(zip(merits_d[1:], materials_d), 1):
ax.annotate(name, (i, m), textcoords="offset points",
xytext=(5, 10), fontsize=8, color="darkred", rotation=30)
ax.set_xlabel("Accepted Iteration")
ax.set_ylabel("Merit Function")
ax.set_title("Dichroic Beamsplitter — Needle Synthesis Convergence")
ax.set_yscale("log")
ax.grid(True, alpha=0.3)
fig.tight_layout();
Optimized stack structure
fig, ax = result_d.stack.plot_structure()
Final spectral performance
The final comparison shows the starting (HL)³ stack vs. the optimized dichroic. The algorithm built a sharp transition edge at 550 nm using all four candidate materials.
# Rebuild starting design for comparison
stack_d_initial = ThinFilmStack(incident_material=air, substrate_material=nbk7)
for _ in range(3):
stack_d_initial.add_layer_nm(tio2, 47.0, name="TiO2")
stack_d_initial.add_layer_nm(sio2, 86.0, name="SiO2")
wl_eval = np.linspace(390, 730, 400)
R_d_initial = np.array([
ThinFilmOperand.reflectance(stack_d_initial, wl) for wl in wl_eval
])
R_d_final = np.array([
ThinFilmOperand.reflectance(result_d.stack, wl) for wl in wl_eval
])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_eval, R_d_initial * 100, "--", color="gray",
linewidth=1.5, label="Starting design (6 layers)")
ax.plot(wl_eval, R_d_final * 100, "-", color="steelblue",
linewidth=2,
label=f"After needle synthesis ({len(result_d.stack.layers)} layers)")
ax.axvspan(420, 540, alpha=0.06, color="blue")
ax.axvspan(560, 680, alpha=0.06, color="red")
ax.axhline(95, color="blue", linestyle=":",
linewidth=1, alpha=0.5, label="R > 95% spec")
ax.axhline(5, color="red", linestyle=":", linewidth=1, alpha=0.5, label="R < 5% spec")
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Dichroic Beamsplitter on N-BK7 — Needle Synthesis Result")
ax.set_ylim(0, 100)
ax.legend(loc="center right")
ax.grid(True, alpha=0.3)
fig.tight_layout();
Show full code listing
import numpy as np
import matplotlib.pyplot as plt
from optiland.materials import Material, IdealMaterial
from optiland.thin_film import ThinFilmStack, SpectralAnalyzer
from optiland.thin_film.optimization import NeedleSynthesis
from optiland.thin_film.optimization.operand.thin_film import ThinFilmOperand
# Incident medium
air = IdealMaterial(n=1.0)
# Substrate
nbk7 = Material("N-BK7")
# Candidate coating materials (dispersive)
sio2 = Material("SiO2", reference="Malitson")
tio2 = Material("TiO2", reference="Devore-o")
mgf2 = Material("MgF2", reference="Dodge-o")
al2o3 = Material("Al2O3", reference="Malitson")
# Print refractive indices at 550 nm
for name, mat in [("N-BK7", nbk7), ("SiO2", sio2), ("TiO2", tio2),
("MgF2", mgf2), ("Al2O3", al2o3)]:
print(f"{name:6s} n(550nm) = {np.asarray(mat.n(0.55)).item():.4f}")
# Starting design: single MgF2 layer
stack = ThinFilmStack(incident_material=air, substrate_material=nbk7)
stack.add_layer_nm(mgf2, 100.0, name="MgF2")
print("\nStarting design:")
print(stack)
wl_plot = np.linspace(400, 720, 300)
R_start = np.array([ThinFilmOperand.reflectance(stack, wl) for wl in wl_plot])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_plot, R_start * 100, "-", color="gray",
linewidth=2, label="Single MgF2 layer")
ax.axhline(1.0, color="red", linestyle="--",
linewidth=1, alpha=0.7, label="Spec: R < 1%")
ax.axvspan(420, 680, alpha=0.06, color="blue", label="Target band (420–680 nm)")
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Starting Design — Single MgF2 Layer on N-BK7")
ax.set_ylim(0, 5)
ax.legend(loc="upper right")
ax.grid(True, alpha=0.3)
fig.tight_layout();
ns = NeedleSynthesis(
stack=stack,
candidate_materials=[sio2, tio2, mgf2, al2o3],
needle_thickness_nm=1.0,
min_thickness_nm=2.0,
max_iterations=12,
num_positions_per_layer=8,
optimizer_max_iter=200,
)
# Dense broadband target: minimize R across 420-680 nm
wavelengths = np.linspace(420, 680, 30).tolist()
ns.add_spectral_target("R", wavelengths, "equal", 0.0)
result = ns.run(verbose=True)
print(f"Success: {result.success}")
print(f"Iterations: {result.num_iterations}")
print(f"Layers added: {result.num_layers_added}")
print(f"Initial merit: {result.initial_merit:.6e}")
print(f"Final merit: {result.final_merit:.6e}")
print(f"Improvement: {(1 - result.final_merit / result.initial_merit) * 100:.1f}%")
print(f"\nFinal stack:")
print(result.stack)
# Check against spec: R < 1% everywhere in 420-680 nm
wl_check = np.linspace(420, 680, 100)
R_vals = np.array([ThinFilmOperand.reflectance(result.stack, wl) for wl in wl_check])
print(f"\nAverage R (420-680 nm): {np.mean(R_vals)*100:.3f}%")
print(f"Peak R (420-680 nm): {np.max(R_vals)*100:.3f}%")
print(f"R < 1% across full band: {np.all(R_vals < 0.01)}")
merits = [result.initial_merit] + [h.merit_after for h in result.history]
materials_used = [h.material_name for h in result.history]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(merits)), merits, "o-", color="steelblue", linewidth=2, markersize=8)
# Annotate each insertion with material name
for i, (m, name) in enumerate(zip(merits[1:], materials_used), 1):
ax.annotate(name, (i, m), textcoords="offset points",
xytext=(5, 10), fontsize=9, color="darkred")
ax.set_xlabel("Accepted Iteration")
ax.set_ylabel("Merit Function")
ax.set_title("Needle Synthesis Convergence")
ax.set_yscale("log")
ax.grid(True, alpha=0.3)
fig.tight_layout();
fig, ax = result.stack.plot_structure()
# Rebuild starting design for comparison
stack_initial = ThinFilmStack(incident_material=air, substrate_material=nbk7)
stack_initial.add_layer_nm(mgf2, 100.0, name="MgF2")
wl_eval = np.linspace(400, 720, 300)
R_initial = np.array([
ThinFilmOperand.reflectance(stack_initial, wl) for wl in wl_eval
])
R_final = np.array([ThinFilmOperand.reflectance(result.stack, wl) for wl in wl_eval])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_eval, R_initial * 100, "--", color="gray",
linewidth=1.5, label="Starting design (1 layer)")
ax.plot(wl_eval, R_final * 100, "-", color="steelblue",
linewidth=2,
label=f"After needle synthesis ({len(result.stack.layers)} layers)")
ax.axhline(1.0, color="red", linestyle="--",
linewidth=1, alpha=0.7, label="Spec: R < 1%")
ax.axvspan(420, 680, alpha=0.06, color="blue")
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Broadband AR on N-BK7 — Needle Synthesis Result")
ax.set_ylim(0, 5)
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout();
# (HL)^3 quarter-wave stack at 500 nm
# QW optical thickness: n*d = 500/4 = 125 nm
# TiO2 (n~2.65): d = 125/2.65 ~ 47 nm
# SiO2 (n~1.46): d = 125/1.46 ~ 86 nm
stack_d = ThinFilmStack(incident_material=air, substrate_material=nbk7)
for _ in range(3):
stack_d.add_layer_nm(tio2, 47.0, name="TiO2")
stack_d.add_layer_nm(sio2, 86.0, name="SiO2")
print(stack_d)
# Plot starting performance
wl_plot = np.linspace(400, 720, 300)
R_start_d = np.array([ThinFilmOperand.reflectance(stack_d, wl) for wl in wl_plot])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_plot, R_start_d * 100, "-", color="gray", linewidth=2)
ax.axvspan(420, 540, alpha=0.08, color="blue", label="Reflect: R > 95%")
ax.axvspan(560, 680, alpha=0.08, color="red", label="Transmit: T > 95%")
ax.axhline(95, color="blue", linestyle="--", linewidth=1, alpha=0.5)
ax.axhline(5, color="red", linestyle="--", linewidth=1, alpha=0.5)
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Starting Design — (HL)³ Quarter-Wave Stack on N-BK7")
ax.set_ylim(0, 100)
ax.legend(loc="center right")
ax.grid(True, alpha=0.3)
fig.tight_layout();
ns_d = NeedleSynthesis(
stack=stack_d,
candidate_materials=[sio2, tio2, mgf2, al2o3],
needle_thickness_nm=1.0,
min_thickness_nm=2.0,
max_iterations=8,
num_positions_per_layer=3,
optimizer_max_iter=80,
)
# Reflection band: R → 1 for 420-540 nm
wl_reflect = np.linspace(420, 540, 10).tolist()
ns_d.add_spectral_target("R", wl_reflect, "equal", 1.0)
# Transmission band: R → 0 for 560-680 nm
wl_transmit = np.linspace(560, 680, 10).tolist()
ns_d.add_spectral_target("R", wl_transmit, "equal", 0.0)
result_d = ns_d.run(verbose=True)
print(f"Success: {result_d.success}")
print(f"Layers added: {result_d.num_layers_added}")
print(f"Initial merit: {result_d.initial_merit:.6e}")
print(f"Final merit: {result_d.final_merit:.6e}")
improvement_pct = (1 - result_d.final_merit / result_d.initial_merit) * 100
print(f"Improvement: {improvement_pct:.1f}%")
print(f"\nFinal stack ({len(result_d.stack.layers)} layers):")
print(result_d.stack)
# Check against specs
wl_r = np.linspace(420, 540, 50)
wl_t = np.linspace(560, 680, 50)
R_reflect = np.array([ThinFilmOperand.reflectance(result_d.stack, wl) for wl in wl_r])
R_transmit = np.array([ThinFilmOperand.reflectance(result_d.stack, wl) for wl in wl_t])
print(f"\nReflection band (420-540 nm):")
print(f" Average R: {np.mean(R_reflect)*100:.1f}%")
print(f" Min R: {np.min(R_reflect)*100:.1f}%")
print(f"\nTransmission band (560-680 nm):")
print(f" Average T: {(1-np.mean(R_transmit))*100:.1f}%")
print(f" Min T: {(1-np.max(R_transmit))*100:.1f}%")
merits_d = [result_d.initial_merit] + [h.merit_after for h in result_d.history]
materials_d = [h.material_name for h in result_d.history]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(
range(len(merits_d)), merits_d,
"o-", color="steelblue", linewidth=2, markersize=8
)
for i, (m, name) in enumerate(zip(merits_d[1:], materials_d), 1):
ax.annotate(name, (i, m), textcoords="offset points",
xytext=(5, 10), fontsize=8, color="darkred", rotation=30)
ax.set_xlabel("Accepted Iteration")
ax.set_ylabel("Merit Function")
ax.set_title("Dichroic Beamsplitter — Needle Synthesis Convergence")
ax.set_yscale("log")
ax.grid(True, alpha=0.3)
fig.tight_layout();
fig, ax = result_d.stack.plot_structure()
# Rebuild starting design for comparison
stack_d_initial = ThinFilmStack(incident_material=air, substrate_material=nbk7)
for _ in range(3):
stack_d_initial.add_layer_nm(tio2, 47.0, name="TiO2")
stack_d_initial.add_layer_nm(sio2, 86.0, name="SiO2")
wl_eval = np.linspace(390, 730, 400)
R_d_initial = np.array([
ThinFilmOperand.reflectance(stack_d_initial, wl) for wl in wl_eval
])
R_d_final = np.array([
ThinFilmOperand.reflectance(result_d.stack, wl) for wl in wl_eval
])
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(wl_eval, R_d_initial * 100, "--", color="gray",
linewidth=1.5, label="Starting design (6 layers)")
ax.plot(wl_eval, R_d_final * 100, "-", color="steelblue",
linewidth=2,
label=f"After needle synthesis ({len(result_d.stack.layers)} layers)")
ax.axvspan(420, 540, alpha=0.06, color="blue")
ax.axvspan(560, 680, alpha=0.06, color="red")
ax.axhline(95, color="blue", linestyle=":",
linewidth=1, alpha=0.5, label="R > 95% spec")
ax.axhline(5, color="red", linestyle=":", linewidth=1, alpha=0.5, label="R < 5% spec")
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("Reflectance (%)")
ax.set_title("Dichroic Beamsplitter on N-BK7 — Needle Synthesis Result")
ax.set_ylim(0, 100)
ax.legend(loc="center right")
ax.grid(True, alpha=0.3)
fig.tight_layout();Conclusions
- The needle synthesis algorithm automatically grows a multilayer stack by inserting thin "needle" layers one at a time, selecting the material and position that most reduces the merit function at each iteration — removing the need to specify the number of layers in advance.
- Starting from a single MgF2 quarter-wave layer, the algorithm reached the stringent R < 1% broadband AR specification across 420–680 nm on N-BK7, a target that is impossible to meet with any single-layer design.
- For the much harder dichroic beamsplitter problem, the algorithm built a sharp 550 nm transition edge from a 6-layer quarter-wave seed, autonomously selecting from four candidate materials (SiO2, TiO2, MgF2, Al2O3) to simultaneously satisfy R > 95% and T > 95% specs in separate spectral bands.
- Logging the merit function after each accepted insertion produces a convergence history that reveals which materials the algorithm preferred and how quickly the design improved, providing interpretable insight into the layer-growth process.
- The
NeedleSynthesisclass integrates seamlessly with theThinFilmStackecosystem: theresult.stackoutput is a standard stack object that can be passed directly toSpectralAnalyzer, plotted withplot_structure, or handed toThinFilmOptimizerfor further refinement.
Next tutorials
Original notebook: Tutorial_6h_Needle_Synthesis.ipynb on GitHub · ReadTheDocs