Bayesian optimization of a SOI vertical grating coupler

Bayesian optimization of a SOI vertical grating coupler#

Grating couplers are photonic devices that couple light efficiently between optical fibers and integrated photonic waveguides. They work by diffracting light incident from above into the plane of the chip, enabling vertical fiber-to-chip coupling—a key functionality for scalable photonic integrated circuits. Vertical grating couplers are widely used because they simplify packaging and alignment compared to edge coupling, and can be designed with high efficiency for target wavelengths, such as the telecom band around 1.55 μm.

This notebook demonstrates the design optimization of a vertical grating coupler using the Bayesian optimizer from Tidy3D’s design plugin. The optimized design achieves a < 2dB coupling efficiency. The design is based on the work T. Watanabe, M. Ayata, U. Koch, Y. Fedoryshyn and J. Leuthold, "Perpendicular Grating Coupler Based on a Blazed Antiback-Reflection Structure," in Journal of Lightwave Technology, vol. 35, no. 21, pp. 4663-4669, 1 Nov.1, 2017. DOI: 10.1109/JLT.2017.2755673.

schematic

[1]:
# Standard library imports
import matplotlib.pyplot as plt
import numpy as np

# Tidy3D imports
import tidy3d as td
import tidy3d.plugins.design as tdd
import tidy3d.web as web

Simulation Setup#

We define the simulation parameters, including the wavelength range, fixed geometric parameters, and the materials (silicon and silicon oxide).

[2]:
# Define frequency and wavelength range
lda0 = 1.55  # Central wavelength
freq0 = td.C_0 / lda0  # Central frequency
ldas = np.linspace(1.5, 1.6, 101)  # Wavelength range
freqs = td.C_0 / ldas  # Frequency range
fwidth = 0.5 * (np.max(freqs) - np.min(freqs))  # Frequency width of the source
[3]:
# Geometric parameters
h_si = 0.22  # Silicon thickness
h_etch = 0.07  # Etch depth
h_box = 3  # Buried oxide thickness
h_clad = 1.5  # Cladding thickness
n = 20  # Number of grating periods
mfd = 10.4  # Mode field diameter
ly = 15  # Grating size in y

# Materials
n_si = 3.45
mat_si = td.Medium(permittivity=n_si**2)

n_sio2 = 1.45
mat_sio2 = td.Medium(permittivity=n_sio2**2)

The grating coupler geometry is parameterized by the thickness and width of the grating teeth (t1, t2, w1, w2, w3). The make_grating function constructs the grating geometry, and make_sim_2d creates the full Tidy3D simulation, including the waveguide, substrate, source, and monitors. We will start our optimization in 2D since it’s much faster and cheaper. Then we will verify the optimal design with the full 3D simulation.

schematic

[4]:
def make_grating(t1, t2, w1, w2, w3):
    """Constructs the grating geometry based on parameters."""
    p = t1 + t2 + w1 + w2 + w3  # Grating period
    grating = 0
    for i in range(n):
        # First tooth section
        grating += td.Box.from_bounds(
            rmin=(i * p + t1, -ly / 2, 0), rmax=(i * p + t1 + w1, ly / 2, h_si)
        )

        # Etched section
        grating += td.Box.from_bounds(
            rmin=(i * p + t1 + w1 + t2, -ly / 2, 0),
            rmax=(i * p + t1 + w1 + t2 + w2, ly / 2, h_si - h_etch),
        )

        # Second tooth section
        grating += td.Box.from_bounds(
            rmin=(i * p + t1 + w1 + t2 + w2, -ly / 2, 0),
            rmax=(i * p + t1 + w1 + t2 + w2 + w3, ly / 2, h_si),
        )

    return td.Structure(geometry=grating, medium=mat_si)
[5]:
def make_sim_2d(t1, t2, w1, w2, w3):
    """Creates the Tidy3D simulation."""
    grating = make_grating(t1, t2, w1, w2, w3)

    p = t1 + t2 + w1 + w2 + w3  # Grating period
    lx = p * n  # Grating size in x
    inf_eff = 1e3  # effective infinity

    # Waveguide
    waveguide = td.Structure(
        geometry=td.Box.from_bounds(rmin=(-inf_eff, -ly / 2, 0), rmax=(0, ly / 2, h_si)),
        medium=mat_si,
    )

    # Cladding and buried oxide
    box_clad = td.Structure(
        geometry=td.Box.from_bounds(
            rmin=(-inf_eff, -inf_eff, -h_box), rmax=(inf_eff, inf_eff, h_clad + h_si)
        ),
        medium=mat_sio2,
    )

    # Substrate
    substrate = td.Structure(
        geometry=td.Box.from_bounds(
            rmin=(-inf_eff, -inf_eff, -h_box - inf_eff), rmax=(inf_eff, inf_eff, -h_box)
        ),
        medium=mat_si,
    )

    # Gaussian beam source
    gaussian_beam = td.GaussianBeam(
        center=(lx / 2, 0, h_clad + lda0 / 2),
        size=(2 * mfd, 2 * mfd, 0),
        source_time=td.GaussianPulse(freq0=freq0, fwidth=fwidth),
        pol_angle=np.pi / 2,
        angle_theta=0,
        angle_phi=0,
        direction="-",
        waist_radius=mfd / 2,
        waist_distance=0,
    )

    # Mode monitor
    mode_monitor = td.ModeMonitor(
        center=(-lda0 / 2, 0, h_si / 2),
        size=(0, 1.5 * ly, 5 * h_si),
        freqs=freqs,
        mode_spec=td.ModeSpec(num_modes=1, target_neff=n_si),
        name="mode",
    )

    # Simulation domain box
    sim_box = td.Box.from_bounds(
        rmin=(-0.6 * lda0, 0, -h_box - 0.6 * lda0),
        rmax=(lx + 0.6 * lda0, 0, h_clad + h_si + 0.6 * lda0),
    )

    # Define the simulation
    sim = td.Simulation(
        center=sim_box.center,
        size=sim_box.size,
        grid_spec=td.GridSpec.auto(min_steps_per_wvl=20),
        run_time=1e-11,
        structures=[substrate, box_clad, grating, waveguide],
        sources=[gaussian_beam],
        monitors=[mode_monitor],
        boundary_spec=td.BoundarySpec(
            x=td.Boundary.pml(),
            y=td.Boundary.periodic(),  # set the boundary to periodic in y since it's a 2D simulation
            z=td.Boundary.pml(),
        ),
    )
    return sim
[6]:
# Create a simulation instance with some arbitrary parameters
sim = make_sim_2d(t1=0.1, t2=0.1, w1=0.1, w2=0.25, w3=0.1)

# Visualize the permittivity distribution
sim.plot_eps(y=0)
plt.show()
../_images/notebooks_VerticalGratingCoupler_14_0.png

Optimization#

Bayesian optimization is a powerful strategy for optimizing expensive-to-evaluate functions, using a probabilistic model to efficiently explore the parameter space and identify optimal solutions with fewer simulations.

To optimize the grating design for maximal coupling efficiency at 1550 nm, we configure the Bayesian optimization using MethodBayOpt.

We define the design space for the grating parameters and specify the objective function compute_transmission, which calculates the transmission efficiency based on the mode monitor data.

[7]:
def compute_transmission(sim_data):
    """Calculates the coupling efficiency from the simulation data."""
    # Extract mode amplitude data at 1550 nm
    T = np.abs(sim_data["mode"].amps.sel(mode_index=0, f=freq0, direction="-").values) ** 2
    # Return coupling efficiency in dB
    return 10 * np.log10(T)

To ensure fabrication constraint, we limit the minimal feature size to 50 nm. All design parameters have a range from 50 nm to 250 nm.

After running the optimization, we achieved a design with sub 2dB coupling efficiency.

[8]:
# Define optimization method (Bayesian optimization)
method = tdd.MethodBayOpt(
    initial_iter=50,
    n_iter=150,
    acq_func="ucb",
    kappa=1,
    seed=4,
)

span = (0.05, 0.25)  # Parameter value range

# Define optimization parameters
parameters = [
    tdd.ParameterFloat(name="t1", span=span),
    tdd.ParameterFloat(name="t2", span=span),
    tdd.ParameterFloat(name="w1", span=span),
    tdd.ParameterFloat(name="w2", span=span),
    tdd.ParameterFloat(name="w3", span=span),
]

# Define a design space
design_space = tdd.DesignSpace(
    method=method, parameters=parameters, task_name="bay_opt", path_dir="./data"
)

# Run the design optimization
results = design_space.run(make_sim_2d, compute_transmission, verbose=True)
14:23:27 UTC Running 50 Simulations
14:24:29 UTC Best Fit from Initial Solutions: -7.699
             
             Running 1 Simulations
14:24:32 UTC Running 1 Simulations
14:24:36 UTC Running 1 Simulations
14:24:39 UTC Running 1 Simulations
14:24:43 UTC Running 1 Simulations
14:24:48 UTC Running 1 Simulations
14:24:52 UTC Running 1 Simulations
14:24:55 UTC Running 1 Simulations
14:24:58 UTC Latest Best Fit on Iter 7: -7.638
             
14:24:59 UTC Running 1 Simulations
14:25:02 UTC Running 1 Simulations
14:25:06 UTC Latest Best Fit on Iter 9: -6.465
             
             Running 1 Simulations
14:25:09 UTC Running 1 Simulations
14:25:13 UTC Latest Best Fit on Iter 11: -6.084
             
             Running 1 Simulations
14:25:17 UTC Running 1 Simulations
14:25:20 UTC Running 1 Simulations
14:25:24 UTC Latest Best Fit on Iter 14: -5.192
             
             Running 1 Simulations
14:25:27 UTC Latest Best Fit on Iter 15: -4.784
             
             Running 1 Simulations
14:25:31 UTC Running 1 Simulations
14:25:34 UTC Running 1 Simulations
14:25:37 UTC Running 1 Simulations
14:25:41 UTC Running 1 Simulations
14:25:44 UTC Running 1 Simulations
14:25:48 UTC Running 1 Simulations
14:25:52 UTC Running 1 Simulations
14:25:55 UTC Latest Best Fit on Iter 23: -2.823
             
             Running 1 Simulations
14:25:59 UTC Running 1 Simulations
14:26:02 UTC Running 1 Simulations
14:26:06 UTC Running 1 Simulations
14:26:09 UTC Running 1 Simulations
14:26:13 UTC Running 1 Simulations
14:26:16 UTC Running 1 Simulations
14:26:20 UTC Running 1 Simulations
14:26:23 UTC Running 1 Simulations
14:26:27 UTC Running 1 Simulations
14:26:40 UTC Running 1 Simulations
14:26:44 UTC Running 1 Simulations
14:26:48 UTC Latest Best Fit on Iter 35: -2.496
             
             Running 1 Simulations
14:26:51 UTC Latest Best Fit on Iter 36: -2.38
             
14:26:52 UTC Running 1 Simulations
14:26:55 UTC Running 1 Simulations
14:26:59 UTC Running 1 Simulations
14:27:02 UTC Latest Best Fit on Iter 39: -2.285
             
             Running 1 Simulations
14:27:06 UTC Latest Best Fit on Iter 40: -2.236
             
             Running 1 Simulations
14:27:10 UTC Running 1 Simulations
14:27:13 UTC Running 1 Simulations
14:27:17 UTC Latest Best Fit on Iter 43: -2.174
             
             Running 1 Simulations
14:27:21 UTC Running 1 Simulations
14:27:24 UTC Running 1 Simulations
14:27:28 UTC Running 1 Simulations
14:27:31 UTC Running 1 Simulations
14:27:35 UTC Running 1 Simulations
14:27:38 UTC Running 1 Simulations
14:27:43 UTC Running 1 Simulations
14:27:46 UTC Latest Best Fit on Iter 51: -2.084
             
             Running 1 Simulations
14:27:49 UTC Latest Best Fit on Iter 52: -2.003
             
14:27:50 UTC Running 1 Simulations
14:27:53 UTC Running 1 Simulations
14:27:56 UTC Running 1 Simulations
14:28:00 UTC Running 1 Simulations
14:28:03 UTC Running 1 Simulations
14:28:07 UTC Latest Best Fit on Iter 57: -1.94
             
             Running 1 Simulations
14:28:11 UTC Running 1 Simulations
14:28:14 UTC Running 1 Simulations
14:28:18 UTC Running 1 Simulations
14:28:21 UTC Running 1 Simulations
14:28:25 UTC Running 1 Simulations
14:28:28 UTC Running 1 Simulations
14:28:32 UTC Running 1 Simulations
14:28:35 UTC Running 1 Simulations
14:28:38 UTC Running 1 Simulations
14:28:42 UTC Running 1 Simulations
14:28:45 UTC Running 1 Simulations
14:28:49 UTC Running 1 Simulations
14:28:52 UTC Running 1 Simulations
14:28:56 UTC Running 1 Simulations
14:28:59 UTC Running 1 Simulations
14:29:03 UTC Running 1 Simulations
14:29:06 UTC Running 1 Simulations
14:29:10 UTC Running 1 Simulations
14:29:13 UTC Running 1 Simulations
14:29:17 UTC Running 1 Simulations
14:29:21 UTC Running 1 Simulations
14:29:24 UTC Running 1 Simulations
14:29:27 UTC Running 1 Simulations
14:29:31 UTC Running 1 Simulations
14:29:34 UTC Running 1 Simulations
14:29:38 UTC Running 1 Simulations
14:29:42 UTC Running 1 Simulations
14:29:45 UTC Running 1 Simulations
14:29:49 UTC Running 1 Simulations
14:29:52 UTC Running 1 Simulations
14:29:56 UTC Running 1 Simulations
14:30:00 UTC Running 1 Simulations
14:30:03 UTC Running 1 Simulations
14:30:07 UTC Running 1 Simulations
14:30:10 UTC Running 1 Simulations
14:30:14 UTC Running 1 Simulations
14:30:18 UTC Running 1 Simulations
14:30:21 UTC Running 1 Simulations
14:30:25 UTC Running 1 Simulations
14:30:29 UTC Running 1 Simulations
14:30:32 UTC Running 1 Simulations
14:30:36 UTC Running 1 Simulations
14:30:39 UTC Running 1 Simulations
14:30:44 UTC Running 1 Simulations
14:30:47 UTC Running 1 Simulations
14:30:51 UTC Running 1 Simulations
14:30:54 UTC Running 1 Simulations
14:30:58 UTC Running 1 Simulations
14:31:02 UTC Running 1 Simulations
14:31:06 UTC Running 1 Simulations
14:31:09 UTC Running 1 Simulations
14:31:13 UTC Running 1 Simulations
14:31:16 UTC Running 1 Simulations
14:31:20 UTC Running 1 Simulations
14:31:24 UTC Running 1 Simulations
14:31:27 UTC Running 1 Simulations
14:31:31 UTC Running 1 Simulations
14:31:34 UTC Running 1 Simulations
14:31:38 UTC Running 1 Simulations
14:31:41 UTC Running 1 Simulations
14:31:45 UTC Running 1 Simulations
14:31:48 UTC Running 1 Simulations
14:31:52 UTC Running 1 Simulations
14:31:55 UTC Running 1 Simulations
14:31:59 UTC Running 1 Simulations
14:32:02 UTC Running 1 Simulations
14:32:06 UTC Running 1 Simulations
14:32:09 UTC Latest Best Fit on Iter 125: -1.826
             
14:32:10 UTC Running 1 Simulations
14:32:13 UTC Running 1 Simulations
14:32:17 UTC Running 1 Simulations
14:32:21 UTC Running 1 Simulations
14:32:24 UTC Running 1 Simulations
14:32:28 UTC Latest Best Fit on Iter 130: -1.759
             
             Running 1 Simulations
14:32:32 UTC Running 1 Simulations
14:32:35 UTC Running 1 Simulations
14:32:39 UTC Running 1 Simulations
14:32:42 UTC Running 1 Simulations
14:32:46 UTC Running 1 Simulations
14:32:49 UTC Running 1 Simulations
14:32:54 UTC Running 1 Simulations
14:32:57 UTC Running 1 Simulations
14:33:01 UTC Running 1 Simulations
14:33:05 UTC Running 1 Simulations
14:33:08 UTC Running 1 Simulations
14:33:12 UTC Running 1 Simulations
14:33:15 UTC Running 1 Simulations
14:33:19 UTC Running 1 Simulations
14:33:22 UTC Running 1 Simulations
14:33:26 UTC Running 1 Simulations
14:33:29 UTC Running 1 Simulations
14:33:33 UTC Running 1 Simulations
14:33:36 UTC Best Result: -1.759101800459121
             Best Parameters: t1: 0.05 t2: 0.05884944980504611 w1:
             0.1015323031540251 w2: 0.24139555731212883 w3: 0.17599800331530713
             

Final Design#

After the optimization runs, we extract the best parameters and then run a 3D final simulation with these optimal parameters to verify the design from the previous 2D simulations. The 3D simulation can be made by copying the 2D simulation and updating the simulation domain size and boundary condition. To help with visualization, we also added two FieldMonitors in the \(xy\) and \(xz\) planes. Also note that the 3D simulation has a symmetry in the \(y\) direction that we can utilize to reduce the computational cost and time.

[9]:
# Define field monitors for visualization
field_xy = td.FieldMonitor(
    center=(0, 0, h_si / 2), size=(td.inf, td.inf, 0), freqs=[freq0], name="field_xy"
)

field_xz = td.FieldMonitor(
    center=(0, 0, 0), size=(td.inf, 0, td.inf), freqs=[freq0], name="field_xz"
)

# Extract optimal parameters from the results
optimal_params = results.optimizer.max["params"]

# Make a 2D simulation with the optimal parameters
sim_opt = make_sim_2d(**optimal_params)

# Update simulation with monitors and new domain size to make it 3D
sim_opt = sim_opt.updated_copy(
    size=(sim_opt.size[0], ly + 1.2 * lda0, sim_opt.size[2]),
    monitors=[sim_opt.monitors[0], field_xy, field_xz],
    boundary_spec=td.BoundarySpec.all_sides(boundary=td.PML()),
    symmetry=(0, -1, 0),
)

# Plot the permittivity distribution of the optimal design
fig, ax = plt.subplots(2, 1, figsize=(8, 8), tight_layout=True)

# Plot the permittivity distribution in the xy plane
sim_opt.plot_eps(z=h_si / 2, ax=ax[0], monitor_alpha=0.1)

# Add a circle to indicate the mode field diameter
lx = sum(optimal_params.values()) * n
circle = plt.Circle((lx / 2, 0), mfd / 2, color="red", alpha=0.6, fill=True)
ax[0].add_patch(circle)

# Plot the permittivity distribution in the xz plane
sim_opt.plot_eps(y=0, ax=ax[1], monitor_alpha=0.1)
plt.show()
../_images/notebooks_VerticalGratingCoupler_20_0.png

Run the 3D simulation and plot the coupling efficiency spectrum.

[10]:
# Run the 3D simulation
sim_data_opt = web.run(sim_opt, "optimal design")

# extract coupling efficiency and plot it
amp = sim_data_opt["mode"].amps.sel(mode_index=0, direction="-")
T = np.abs(amp) ** 2
plt.plot(ldas, 10 * np.log10(T), c="red", linewidth=2)
plt.ylim(-12, 0)
plt.xlabel("Wavelength (μm)")
plt.ylabel("Coupling efficiency (dB)")
plt.grid()
plt.show()
14:33:37 UTC Created task 'optimal design' with resource_id
             'fdve-a1b05b7d-388b-40be-84cb-5f3d394bf317' and task_type 'FDTD'.
             Task folder: 'default'.
14:33:38 UTC Estimated FlexCredit cost: 3.649. This assumes the FDTD solver runs
             for the full simulation time; if early shutoff is reached, the
             billed cost can be lower. Use 'web.real_cost(task_id)' to get the
             billed FlexCredit cost after a simulation run.
14:33:39 UTC status = success
14:33:41 UTC Loading results from simulation_data.hdf5
../_images/notebooks_VerticalGratingCoupler_22_10.png

Finally, plot the field distribution to visualize the mode coupling.

[11]:
fig, ax = plt.subplots(2, 1, figsize=(6, 8), tight_layout=True)
sim_data_opt.plot_field("field_xy", "E", "abs", ax=ax[0])
sim_data_opt.plot_field("field_xz", "E", "abs", ax=ax[1])
plt.show()
../_images/notebooks_VerticalGratingCoupler_24_0.png

Final Notes#

The grating coupler is typically connected to a waveguide taper, which enables an adiabatic transition into a single-mode waveguide. In our simulation, we omit the taper to reduce computational cost. The taper itself can be optimized independently to ensure minimal loss, thereby maintaining a high overall coupling efficiency for the complete device.

In addition to Bayesian optimization, alternative optimization strategies, such as particle swarm optimization (PSO) and adjoint-based inverse design, can also be used to further improve device performance, for example through apodization.