Grating Coupler: Bayesian Optimization for Initial Design#
With our simulation setup in place, we now turn to optimization. Our goal is to find a set of grating parameters that maximizes the coupling efficiency. Since each simulation is computationally expensive, we will use Bayesian optimization. This technique is ideal for optimizing “black-box” functions that are costly to evaluate.
Why Bayesian Optimization?#
Exhaustive searches would require thousands of simulations. Bayesian optimization instead builds a probabilistic surrogate of the objective, balancing exploration of uncertain regions with exploitation of promising designs to converge in far fewer solver calls. It intelligently explores the parameter space to find the optimal design with a minimal number of simulations. Bayesian optimization works best when the design space has only a handful of effective degrees of freedom; beyond roughly five independent variables the surrogate becomes harder to learn, so we reserve higher-dimensional searches for gradient-based methods discussed later in the series.
[ ]:
import matplotlib.pyplot as plt
import numpy as np
import tidy3d as td
from bayes_opt import BayesianOptimization
from setup import (
center_wavelength,
get_mode_monitor_power,
make_simulation,
max_gap_si,
max_gap_sin,
max_width_si,
max_width_sin,
min_gap_si,
min_gap_sin,
min_width_si,
min_width_sin,
num_elements,
)
from setup import (
first_gap_si as default_first_gap_si,
)
from tidy3d import web
The Evaluation Function#
The optimizer queries this function with a candidate set of grating parameters. We construct the simulation, run it in the cloud, and return the coupling efficiency from the mode monitor.
[2]:
def evaluate(
width_si: float,
gap_si: float,
width_sin: float,
gap_sin: float,
first_gap_si: float,
) -> float:
"""Return the coupling efficiency for a uniform grating parameterized array."""
widths_si = np.full(num_elements, width_si)
gaps_si = np.full(num_elements, gap_si)
widths_sin = np.full(num_elements, width_sin)
gaps_sin = np.full(num_elements, gap_sin)
sim = make_simulation(
widths_si,
gaps_si,
widths_sin,
gaps_sin,
first_gap_si=first_gap_si,
)
sim_data = web.run(sim, task_name="gc_bopt_eval", verbose=False)
power_da = get_mode_monitor_power(sim_data)
target_power = power_da.sel(f=td.C_0 / center_wavelength, method="nearest").item()
return target_power
Setting Up the Bayesian Optimizer#
We configure the optimizer with sensible defaults and practical bounds:
parameter_bounds(thepboundsargument) defines the design window we explore.init_pointssets how many random samples to collect before modeling.n_itercontrols the number of guided optimization iterations.
Framing the Problem: A 5-Parameter Global Search#
Rather than tune every tooth individually (30 variables per layer), we search a five-dimensional space of uniform widths, gaps, and inter-layer offset. This captures the dominant physics, keeps simulations fast, and yields a design that later gradient-based passes can refine.
[3]:
seed = 12345
init_points = 15
n_iter = 60
parameter_bounds = {
"width_si": (min_width_si, max_width_si),
"gap_si": (min_gap_si, max_gap_si),
"width_sin": (min_width_sin, max_width_sin),
"gap_sin": (min_gap_sin, max_gap_sin),
"first_gap_si": (
default_first_gap_si - 0.2,
default_first_gap_si + 0.2,
),
}
default_design = {
"width_si": 0.45,
"gap_si": 0.55,
"width_sin": 0.35,
"gap_sin": 0.65,
"first_gap_si": default_first_gap_si,
}
[4]:
optimizer = BayesianOptimization(
f=evaluate,
pbounds=parameter_bounds,
random_state=seed,
verbose=2,
)
optimizer.probe(params=default_design, lazy=True)
Running the Optimization#
Calling optimizer.maximize(...) alternates between exploration and exploitation to efficiently discover improved grating designs.
[5]:
optimizer.maximize(init_points=init_points, n_iter=n_iter)
| iter | target | first_... | gap_si | gap_sin | width_si | width_sin |
-------------------------------------------------------------------------------------
| 1 | 0.007373 | -0.7 | 0.55 | 0.65 | 0.45 | 0.35 |
| 2 | 0.001166 | -0.5282 | 0.4531 | 0.4287 | 0.2841 | 0.6542 |
| 3 | 0.002181 | -0.6618 | 0.9716 | 0.7572 | 0.774 | 0.7229 |
| 4 | 0.04058 | -0.6009 | 0.969 | 0.3059 | 0.1958 | 0.439 |
| 5 | 0.0007281 | -0.6374 | 0.8479 | 0.9105 | 0.9682 | 0.7789 |
| 6 | 0.181 | -0.643 | 0.774 | 0.6273 | 0.393 | 0.5517 |
| 7 | 0.00206 | -0.6081 | 0.9952 | 0.7738 | 0.8117 | 0.3367 |
| 8 | 0.0008147 | -0.8893 | 0.8403 | 0.9326 | 0.1222 | 0.5934 |
| 9 | 0.01132 | -0.6895 | 0.6771 | 0.3364 | 0.9056 | 0.7826 |
| 10 | 0.009404 | -0.5727 | 0.6002 | 0.8671 | 0.1864 | 0.3752 |
| 11 | 0.02459 | -0.7965 | 0.5745 | 0.6216 | 0.7386 | 0.3424 |
| 12 | 0.004018 | -0.6874 | 0.3342 | 0.8382 | 0.9354 | 0.6876 |
| 13 | 0.000441 | -0.8399 | 0.5917 | 0.5641 | 0.8637 | 0.9289 |
| 14 | 8.261e-05 | -0.7465 | 0.4524 | 0.6979 | 0.269 | 0.3007 |
| 15 | 0.001766 | -0.625 | 0.8397 | 0.7015 | 0.9759 | 0.7072 |
| 16 | 0.02805 | -0.5446 | 0.5963 | 0.5461 | 0.7428 | 0.6031 |
| 17 | 0.1787 | -0.6397 | 0.7773 | 0.6306 | 0.3964 | 0.555 |
| 18 | 0.3217 | -0.6909 | 0.8335 | 0.5581 | 0.3556 | 0.5659 |
| 19 | 0.2231 | -0.7598 | 0.8658 | 0.5306 | 0.3545 | 0.6126 |
| 20 | 0.3219 | -0.6909 | 0.8335 | 0.5583 | 0.3559 | 0.5661 |
| 21 | 0.0415 | -0.6376 | 0.9131 | 0.5274 | 0.3905 | 0.5385 |
| 22 | 0.1226 | -0.7002 | 0.8083 | 0.5893 | 0.3262 | 0.6175 |
| 23 | 0.3379 | -0.7272 | 0.8219 | 0.555 | 0.3927 | 0.5517 |
| 24 | 0.3426 | -0.6933 | 0.7992 | 0.5135 | 0.3983 | 0.5782 |
| 25 | 0.01591 | -0.716 | 0.8449 | 0.427 | 0.854 | 0.7467 |
| 26 | 0.2208 | -0.7426 | 0.7811 | 0.4777 | 0.3799 | 0.5267 |
| 27 | 0.1707 | -0.7213 | 0.8082 | 0.5425 | 0.47 | 0.6034 |
| 28 | 0.02944 | -0.6112 | 0.7241 | 0.3802 | 0.1305 | 0.7133 |
| 29 | 0.295 | -0.6724 | 0.7966 | 0.5351 | 0.4015 | 0.5239 |
| 30 | 0.2459 | -0.6254 | 0.7534 | 0.4725 | 0.3783 | 0.5916 |
| 31 | 0.2677 | -0.696 | 0.8142 | 0.4183 | 0.3826 | 0.6427 |
| 32 | 0.1886 | -0.6629 | 0.7435 | 0.3459 | 0.4558 | 0.6294 |
| 33 | 0.2116 | -0.8001 | 0.8154 | 0.3366 | 0.3902 | 0.7036 |
| 34 | 0.1522 | -0.6654 | 0.8274 | 0.3739 | 0.4143 | 0.7777 |
| 35 | 0.02097 | -0.7357 | 0.8873 | 0.3 | 0.4083 | 0.6054 |
| 36 | 0.1153 | -0.7497 | 0.7184 | 0.4285 | 0.3821 | 0.6787 |
| 37 | 0.3122 | -0.7625 | 0.8559 | 0.6204 | 0.3704 | 0.485 |
| 38 | 0.222 | -0.8684 | 0.8764 | 0.6099 | 0.3932 | 0.4762 |
| 39 | 0.1586 | -0.7685 | 0.8767 | 0.7177 | 0.428 | 0.4673 |
| 40 | 0.06846 | -0.7872 | 0.8409 | 0.5931 | 0.2975 | 0.4117 |
| 41 | 0.08975 | -0.7685 | 0.8588 | 0.5832 | 0.4436 | 0.4799 |
| 42 | 0.1084 | -0.7878 | 0.8762 | 0.6417 | 0.3469 | 0.5454 |
| 43 | 0.2467 | -0.6835 | 0.8234 | 0.4741 | 0.3515 | 0.5887 |
| 44 | 0.006366 | -0.5162 | 0.5468 | 0.664 | 0.7499 | 0.8926 |
| 45 | 0.2918 | -0.7085 | 0.8233 | 0.6025 | 0.3602 | 0.4973 |
| 46 | 0.2756 | -0.641 | 0.8088 | 0.4759 | 0.4281 | 0.6529 |
| 47 | 0.259 | -0.5791 | 0.8002 | 0.4016 | 0.3924 | 0.6605 |
| 48 | 0.09593 | -0.5623 | 0.7689 | 0.4404 | 0.4795 | 0.6131 |
| 49 | 0.06088 | -0.6232 | 0.8136 | 0.4617 | 0.3479 | 0.6981 |
| 50 | 0.1864 | -0.6742 | 0.8025 | 0.4417 | 0.4322 | 0.5883 |
| 51 | 0.001129 | -0.7316 | 0.8609 | 0.8731 | 0.7566 | 0.7094 |
| 52 | 0.2293 | -0.7121 | 0.7631 | 0.5474 | 0.3747 | 0.5568 |
| 53 | 0.3059 | -0.6423 | 0.8069 | 0.5383 | 0.4062 | 0.5966 |
| 54 | 0.06258 | -0.5826 | 0.7606 | 0.3205 | 0.3611 | 0.638 |
| 55 | 0.207 | -0.7387 | 0.8525 | 0.4576 | 0.4255 | 0.6778 |
| 56 | 0.04347 | -0.6178 | 0.8528 | 0.3955 | 0.4493 | 0.6773 |
| 57 | 0.2797 | -0.6946 | 0.8403 | 0.5222 | 0.4 | 0.6144 |
| 58 | 0.2402 | -0.7916 | 0.827 | 0.4341 | 0.3574 | 0.6185 |
| 59 | 0.04937 | -0.836 | 0.8689 | 0.4237 | 0.3393 | 0.7152 |
| 60 | 0.3044 | -0.7068 | 0.8805 | 0.6484 | 0.3592 | 0.4604 |
| 61 | 0.0761 | -0.6435 | 0.8796 | 0.4544 | 0.2933 | 0.2807 |
| 62 | 0.2577 | -0.7314 | 0.8167 | 0.6713 | 0.3653 | 0.4545 |
| 63 | 0.003973 | -0.6534 | 0.4818 | 0.9379 | 0.1868 | 0.9214 |
| 64 | 0.2642 | -0.6335 | 0.8656 | 0.7031 | 0.3425 | 0.443 |
| 65 | 0.2023 | -0.6883 | 0.9274 | 0.7084 | 0.345 | 0.3905 |
| 66 | 0.2385 | -0.6441 | 0.8423 | 0.6453 | 0.3966 | 0.4068 |
| 67 | 0.115 | -0.6291 | 0.8525 | 0.6278 | 0.2939 | 0.4472 |
| 68 | 0.009544 | -0.6876 | 0.6745 | 0.6504 | 0.7635 | 0.534 |
| 69 | 0.1545 | -0.6343 | 0.8937 | 0.7117 | 0.4213 | 0.4706 |
| 70 | 0.1657 | -0.6473 | 0.8122 | 0.7514 | 0.3606 | 0.3789 |
| 71 | 0.09981 | -0.6858 | 0.8804 | 0.7599 | 0.3 | 0.4805 |
| 72 | 0.2367 | -0.7438 | 0.8586 | 0.541 | 0.3547 | 0.5173 |
| 73 | 0.1969 | -0.5551 | 0.9023 | 0.7058 | 0.3568 | 0.3739 |
| 74 | 0.003964 | -0.867 | 0.5866 | 0.3316 | 0.3515 | 0.2023 |
| 75 | 0.2389 | -0.7877 | 0.7523 | 0.3 | 0.4788 | 0.7533 |
| 76 | 0.04023 | -0.797 | 0.7835 | 0.3 | 0.4151 | 0.8371 |
=====================================================================================
Analyzing the Results#
We extract the optimizer history, track the best observed loss, and visualize how the search converges toward high-efficiency gratings.
[6]:
best = optimizer.max
results = optimizer.res
iterations = np.arange(1, len(results) + 1)
targets = np.asarray([res["target"] for res in results], dtype=float)
targets = np.maximum(targets, 1e-12)
coupling_loss_db = -10 * np.log10(targets)
best_loss = np.minimum.accumulate(coupling_loss_db)
best_loss_db = -10 * np.log10(max(best["target"], 1e-12))
print("Optimization complete.")
print(f"Best parameters: {best['params']}")
print(f"Best objective (power): {best['target']}")
print(f"Best objective (dB): {best_loss_db:.2f}")
Optimization complete.
Best parameters: {'first_gap_si': np.float64(-0.6933388041768698), 'gap_si': np.float64(0.7992416233438039), 'gap_sin': np.float64(0.5135103145142313), 'width_si': np.float64(0.3983180007432449), 'width_sin': np.float64(0.5781958117277934)}
Best objective (power): 0.3425821844561507
Best objective (dB): 4.65
Interpreting the Optimization Progress#
The scatter points show every simulation the optimizer evaluated, while the red curve tracks the best coupling loss found so far. Early iterations explore widely; later ones cluster near promising regions as the surrogate model focuses on exploitation.
[7]:
fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(iterations, coupling_loss_db, label="Samples")
ax.plot(iterations, best_loss, color="red", label="Best so far")
ax.set_xlabel("Iteration")
ax.set_ylabel("Coupling loss (dB)")
ax.set_title("Bayesian optimization progress")
ax.legend()
plt.grid(True, alpha=0.3)
plt.show()
Visualizing the Optimized Design#
We reconstruct the best-performing structure, inspect its geometry, and analyze the spectral response to confirm the optimizer’s progress.
[8]:
best_params = {name: float(value) for name, value in best["params"].items()}
best_widths_si = np.full(num_elements, best_params["width_si"])
best_gaps_si = np.full(num_elements, best_params["gap_si"])
best_widths_sin = np.full(num_elements, best_params["width_sin"])
best_gaps_sin = np.full(num_elements, best_params["gap_sin"])
best_first_gap_si = best_params["first_gap_si"]
[9]:
best_sim = make_simulation(
best_widths_si,
best_gaps_si,
best_widths_sin,
best_gaps_sin,
first_gap_si=best_first_gap_si,
include_field_monitor=True,
)
ax = best_sim.plot(y=0)
ax.set_title("Cross-section of the optimized grating (y=0)")
plt.show()
[10]:
best_data = web.run(best_sim, task_name="gc_bopt_final", verbose=False)
[11]:
power_da = get_mode_monitor_power(best_data)
freqs = power_da.coords["f"].values
wavelengths = td.C_0 / freqs
power = np.squeeze(power_da.data)
power_db = 10 * np.log10(power)
[12]:
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(wavelengths, power_db)
ax.set_xlabel("Wavelength (µm)")
ax.set_ylabel("Transmission (dB)")
ax.set_title("Mode monitor spectrum (optimized design)")
ax.grid(True, alpha=0.3)
plt.show()
[13]:
ax = best_data.plot_field("field_monitor", "Ey", "abs^2")
ax.set_title("Field intensity |Ey|^2 for the optimized design")
plt.show()
The optimized geometry increases overlap between the free-space beam and the guided mode, yielding a stronger steady-state field inside the silicon nitride layer. In the next notebook we leverage this design as the starting point for gradient-based refinement.
Exporting the Best Design#
We serialize the best uniform grating parameters so the adjoint notebook can continue from this design without rerunning the Bayesian search.
[14]:
import json
from pathlib import Path
export_path = Path("./results/gc_bayes_opt_best.json")
export_path.parent.mkdir(parents=True, exist_ok=True)
export_payload = {
"width_si": best_params["width_si"],
"gap_si": best_params["gap_si"],
"width_sin": best_params["width_sin"],
"gap_sin": best_params["gap_sin"],
"first_gap_si": best_params["first_gap_si"],
"target_power": float(best["target"]),
"coupling_loss_db": float(best_loss_db),
}
with export_path.open("w", encoding="utf-8") as f:
json.dump(export_payload, f, indent=2)
print(f"Saved best design to {export_path.resolve()}")
Saved best design to /home/yannick/flexcompute/worktrees/seminar_notebooks/docs/notebooks/2025-10-09-invdes-seminar/results/gc_bayes_opt_best.json