Tutorial 4: Parameter Optimization
Time: 40 minutes
Difficulty: Advanced
Prerequisites: Tutorials 1-3
Learning Objectives
Perform parameter sweeps
Optimize device performance
Visualize design space
Find optimal operating points
The Challenge: Optimize a Taper
We’ll optimize a waveguide taper to minimize insertion loss.
Approach
We’ll sweep two parameters:
Taper length (5-20 μm)
Taper shape (linear vs exponential vs polynomial)
Implementation
import numpy as np
import matplotlib.pyplot as plt
from prismo.optimization import ParameterSweep
# Define parameters to sweep
lengths = np.linspace(5e-6, 20e-6, 8) # 8 different lengths
shapes = ['linear', 'exponential', 'polynomial']
# Setup parameter sweep
sweep = ParameterSweep(
parameters={
'length': lengths,
'shape': shapes,
},
metric='insertion_loss', # What to optimize
)
# Define simulation function
def simulate_taper(length, shape):
"""Run simulation for given taper parameters."""
# Create taper with specified parameters
taper = create_taper(length=length, shape=shape)
# Setup and run simulation
sim = setup_simulation(taper)
sim.run(100e-15)
# Extract insertion loss
s_params = extract_s_parameters(sim)
insertion_loss = -20 * np.log10(np.abs(s_params['S21']))
return insertion_loss[0] # At design frequency
# Run sweep
results = sweep.run(simulate_taper, parallel=True, n_jobs=4)
# Find optimal parameters
optimal = sweep.get_optimal()
print(f"Optimal parameters:")
print(f" Length: {optimal['length']*1e6:.2f} μm")
print(f" Shape: {optimal['shape']}")
print(f" Insertion Loss: {optimal['metric']:.3f} dB")
# Visualize design space
sweep.plot_2d(
x='length',
y='shape',
metric='insertion_loss',
cmap='viridis_r', # Reverse so dark = better
)
plt.savefig('tutorial4_optimization.png', dpi=150)
plt.show()
Manual Parameter Sweep
For more control, implement sweeps manually:
# Parameter ranges
lengths = np.linspace(5e-6, 20e-6, 10)
widths_in = [2e-6, 3e-6, 4e-6]
# Results storage
results = np.zeros((len(widths_in), len(lengths)))
# Sweep
for i, width_in in enumerate(widths_in):
for j, length in enumerate(lengths):
# Run simulation
il = simulate_taper_config(width_in, length)
results[i, j] = il
print(f" w={width_in*1e6:.1f}μm, L={length*1e6:.1f}μm: IL={il:.3f}dB")
# Plot heatmap
plt.figure(figsize=(10, 6))
plt.imshow(
results,
aspect='auto',
origin='lower',
extent=[lengths[0]*1e6, lengths[-1]*1e6, 0, len(widths_in)],
cmap='RdYlGn_r', # Red = bad, Green = good
)
plt.colorbar(label='Insertion Loss (dB)')
plt.xlabel('Taper Length (μm)')
plt.ylabel('Input Width Configuration')
plt.yticks(range(len(widths_in)), [f'{w*1e6:.1f} μm' for w in widths_in])
plt.title('Taper Optimization: Insertion Loss')
plt.tight_layout()
plt.savefig('manual_sweep.png', dpi=150)
plt.show()
# Find optimal
min_idx = np.unravel_index(np.argmin(results), results.shape)
optimal_width = widths_in[min_idx[0]]
optimal_length = lengths[min_idx[1]]
optimal_loss = results[min_idx]
print(f"\nOptimal design:")
print(f" Width: {optimal_width*1e6:.1f} μm")
print(f" Length: {optimal_length*1e6:.1f} μm")
print(f" Loss: {optimal_loss:.3f} dB")
Gradient-Free Optimization
For complex optimization, use scipy:
from scipy.optimize import minimize
def objective_function(params):
"""Objective to minimize."""
length, width_ratio = params
# Constraints
if length < 5e-6 or length > 25e-6:
return 1000.0 # Penalty
if width_ratio < 1.5 or width_ratio > 4.0:
return 1000.0
# Run simulation
il = simulate_taper_config(length=length, width_ratio=width_ratio)
return il # Minimize insertion loss
# Initial guess
x0 = [10e-6, 2.5] # [length, width_ratio]
# Optimize
result = minimize(
objective_function,
x0,
method='Nelder-Mead', # Gradient-free
options={'maxiter': 50, 'disp': True}
)
print(f"\nOptimized parameters:")
print(f" Length: {result.x[0]*1e6:.2f} μm")
print(f" Width ratio: {result.x[1]:.2f}")
print(f" Final IL: {result.fun:.3f} dB")
Multi-Objective Optimization
Optimize for multiple goals:
def multi_objective(params):
"""Optimize insertion loss AND bandwidth."""
# Run simulation
sim_results = simulate_full(params)
insertion_loss = sim_results['IL']
bandwidth_3db = sim_results['BW']
# Combined objective (weighted sum)
weight_il = 0.7
weight_bw = 0.3
# Normalize and combine
objective = weight_il * insertion_loss - weight_bw * bandwidth_3db
return objective, {'IL': insertion_loss, 'BW': bandwidth_3db}
# Run optimization with multiple objectives
# ... (use Pareto optimization or weighted sum)
Visualization Techniques
1. Contour Plot
# Create 2D grid
L = np.linspace(5e-6, 20e-6, 50)
W = np.linspace(2e-6, 4e-6, 50)
LL, WW = np.meshgrid(L, W)
# Evaluate objective (use fast surrogate model)
Z = np.array([[objective_function([l, w]) for l in L] for w in W])
# Plot
plt.figure(figsize=(10, 8))
contour = plt.contour(LL*1e6, WW*1e6, Z, levels=20, cmap='viridis')
plt.clabel(contour, inline=True, fontsize=8)
plt.xlabel('Length (μm)')
plt.ylabel('Width (μm)')
plt.title('Optimization Landscape')
plt.colorbar(label='Insertion Loss (dB)')
plt.tight_layout()
plt.savefig('contour_plot.png', dpi=150)
plt.show()
2. Parallel Coordinates
import pandas as pd
from pandas.plotting import parallel_coordinates
# Create DataFrame of results
df = pd.DataFrame({
'Length': lengths_tested,
'Width': widths_tested,
'IL': insertion_losses,
'BW': bandwidths,
'Quality': ['Good' if il < 0.5 else 'Bad' for il in insertion_losses]
})
# Plot
plt.figure(figsize=(12, 6))
parallel_coordinates(df, 'Quality', color=['red', 'green'])
plt.ylabel('Normalized Value')
plt.title('Parameter Space Exploration')
plt.tight_layout()
plt.savefig('parallel_coords.png', dpi=150)
plt.show()
Best Practices
Start coarse, refine later: Use wide spacing initially, zoom in on promising regions
Check convergence: Re-run optimal point to verify repeatability
Use surrogate models: For expensive simulations, fit a fast model
Validate physically: Ensure optimal parameters make physical sense
Consider manufacturing: Include fabrication tolerances
Performance Tips
# 1. Parallel execution
from multiprocessing import Pool
def parallel_sweep(params_list):
with Pool(processes=8) as pool:
results = pool.map(simulate_single, params_list)
return results
# 2. Caching results
import pickle
def cached_simulation(params):
cache_file = f"cache_{hash(tuple(params))}.pkl"
if os.path.exists(cache_file):
with open(cache_file, 'rb') as f:
return pickle.load(f)
result = run_simulation(params)
with open(cache_file, 'wb') as f:
pickle.dump(result, f)
return result
# 3. Adaptive sampling
# Focus computational effort on interesting regions
# (implement using Gaussian Processes or similar)
Complete Example: Ring Resonator Optimization
# Optimize ring resonator for maximum Q-factor
def optimize_ring():
# Parameters to optimize
radii = np.linspace(5e-6, 15e-6, 20)
gaps = np.linspace(100e-9, 500e-9, 20)
best_q = 0
best_params = None
for radius in radii:
for gap in gaps:
# Simulate
q_factor = simulate_ring(radius=radius, gap=gap)
if q_factor > best_q:
best_q = q_factor
best_params = (radius, gap)
print(f"Optimal design:")
print(f" Radius: {best_params[0]*1e6:.2f} μm")
print(f" Gap: {best_params[1]*1e9:.0f} nm")
print(f" Q-factor: {best_q:.0f}")
return best_params
optimal = optimize_ring()
Exercises
Optimize a Y-branch splitter for equal power splitting
Find the shortest taper with < 0.1 dB loss
Optimize a grating coupler for maximum efficiency
Multi-objective: minimize loss AND maximize bandwidth
Summary
You’ve learned:
Parameter sweep techniques
Single and multi-objective optimization
Visualization of design spaces
Performance optimization strategies
Key Takeaway: Systematic optimization reveals non-intuitive designs that outperform intuition!
Further Reading
Scipy optimization documentation
Advanced Features - Advanced optimization techniques
Optimization API - Optimization API reference