Inverse design optimization of a compact grating coupler#

This notebook contains a long optimization. Running the entire notebook will cost about 20 FlexCredits and take several hours.

The ability to couple light in and out of photonic integrated circuits (PICs) is crucial for developing wafer-scale systems and tests. This need makes designing efficient and compact grating couplers an important task in the PIC development cycle. In this notebook, we will demonstrate how to use Tidy3D’s adjoint plugin to perform the inverse design of a compact 3D grating coupler. We will show how to improve design fabricability by enhancing permittivity binarization and controlling the device’s minimum feature size.

We start by importing our typical python packages, jax, tidy3d and its adjoint plugin.

[1]:
# Standard python imports.
from typing import List
import numpy as np
import scipy as sp
import matplotlib.pylab as plt
import os
import json
import pydantic as pd
from typing import Callable

# Import jax to be able to use automatic differentiation.
import jax.numpy as jnp
import jax.scipy as jsp
from jax import value_and_grad

# Import regular tidy3d.
import tidy3d as td
import tidy3d.web as web

# Import the components we need from the adjoint plugin.
from tidy3d.plugins.adjoint import (
    JaxSimulation,
    JaxBox,
    JaxCustomMedium,
    JaxStructure,
    JaxSimulationData,
    JaxDataArray,
    JaxPermittivityDataset,
)
from tidy3d.plugins.adjoint.web import run

Grating Coupler Inverse Design Configuration#

The grating coupler inverse design begins with a rectangular design region connected to a \(Si\) waveguide. Throughout the optimization process, this initial structure evolves to convert a vertically incident Gaussian-like mode from an optical fiber into a guided mode and then funnel it into the \(Si\) waveguide.

We are considering a full-etched grating structure, so a \(SiO_{2}\) BOX layer is included. To reduce backreflection, we adjusted the fiber tilt angle to \(10^{\circ}\) [1, 2].

In the following block of code, you can find the parameters that can be modified to configure the grating coupler structure, optimization, and simulation setup. Special care should be devoted to the it_per_step and opt_steps variables bellow.

[2]:
# Geometric parameters.
w_thick = 0.22  # Waveguide thickness (um).
w_width = 0.5  # Waveguide width (um).
w_length = 1.0  # Waveguide length (um).
box_thick = 1.6  # SiO2 BOX thickness (um).
spot_size = 2.5  # Spot size of the input Gaussian field regarding a lensed fiber (um).
fiber_tilt = 10.0  # Fiber tilt angle (degrees).
src_offset = 0.05  # Distance between the source focus and device (um).

# Material.
nSi = 3.48  # Silicon refractive index.
nSiO2 = 1.44  # Silica refractive index.

# Design region parameters.
gc_width = 4.0  # Grating coupler width (um).
gc_length = 4.0  # Grating coupler length (um).
dr_grid_size = 0.02  # Grid size within the design region (um).

# Inverse design set up parameters.
#################################################################
# Total number of iterations = opt_steps x it_per_step.
it_per_step = 10  # Number of iterations per optimization step.
opt_steps = 15 # Number of optimization steps.
#################################################################
feature_size = 0.070 # Minimum feature size (um).
eta = 0.50  # Threshold value for the projection filter.
init_beta = 10  # Sharpness parameter for the projection filter.
del_beta = 10  # Increments of beta at each optimization step.
fom_name = "fom_field"  # Name of the monitor used to compute the objective function.

# Inverse design data file.
output_file = "./misc/invdes_gc.json"

# Simulation wavelength.
wl = 1.55  # Central simulation wavelength (um).
bw = 0.06  # Simulation bandwidth (um).
n_wl = 61  # Number of wavelength points within the bandwidth.

Ckeching For a Previous Optimization#

If output_file is a valid file, the results of a previous optimization are loaded, then the optimization will continue from the last iteration step. If the optimization was completed, only the final structure will be simulated. The JSON file used in this notebook can be downloaded from our documentation repo.

[3]:
total_iter = opt_steps * it_per_step
iter_to_do = total_iter
prev_data = []
if os.path.isfile(output_file):
    with open(output_file, "r", encoding="utf-8") as json_file:
        prev_data = json.load(json_file)[0]
        iter_to_do -= len(prev_data["obj_vals"])
        print("Previous optimization data loaded from " + output_file)

print(f"Total iterations = {total_iter}")
print(f"Iterations to run = {iter_to_do}")

Total iterations = 150
Iterations to run = 150

Inverse Design Optimization Set Up#

We will calculate the values of some parameters used throughout the inverse design set up.

[4]:
# Minimum and maximum values for the permittivities.
eps_max = nSi**2
eps_min = 1.0

# Material definitions.
mat_si = td.Medium(permittivity=eps_max)  # Waveguide material.
mat_sio2 = td.Medium(permittivity=nSiO2**2)  # Substrate material.

# Wavelengths and frequencies.
wl_max = wl + bw / 2
wl_min = wl - bw / 2
wl_range = np.linspace(wl_min, wl_max, n_wl)
freq = td.C_0 / wl
freqs = td.C_0 / wl_range
freqw = 0.5 * (freqs[0] - freqs[-1])
run_time = 5e-12

# Computational domain size.
pml_spacing = 0.6 * wl
size_x = pml_spacing + w_length + gc_length
size_y = gc_width + 2 * pml_spacing
size_z = w_thick + box_thick + 2 * pml_spacing
center_z = size_z / 2 - pml_spacing - w_thick / 2
eff_inf = 1000

# Inverse design variables.
src_pos_z = w_thick / 2 + src_offset
mon_pos_x = -size_x / 2 + 0.25 * wl
mon_w = int(3 * w_width / dr_grid_size) * dr_grid_size
mon_h = int(5 * w_thick / dr_grid_size) * dr_grid_size
nx = int(gc_length / dr_grid_size)
ny = int(gc_width / dr_grid_size / 2.0)
npar = nx * ny
dr_size_x = nx * dr_grid_size
dr_size_y = 2 * ny * dr_grid_size
dr_center_x = -size_x / 2 + w_length + dr_size_x / 2

First, we will introduce the simulation components that do not change during optimization, such as the \(Si\) waveguide and \(SiO_{2}\) BOX layer. Additionally, we will include a Gaussian source to drive the simulations, and a mode monitor to compute the objective function.

[5]:
# Input/output waveguide.
waveguide = td.Structure(
    geometry=td.Box.from_bounds(
        rmin=(-eff_inf, -w_width / 2, -w_thick / 2),
        rmax=(-size_x / 2 + w_length, w_width / 2, w_thick / 2),
    ),
    medium=mat_si,
)

# SiO2 BOX layer.
sio2_substrate = td.Structure(
    geometry=td.Box.from_bounds(
        rmin=(-eff_inf, -eff_inf, -w_thick / 2 - box_thick),
        rmax=(eff_inf, eff_inf, -w_thick / 2)
    ),
    medium=mat_sio2,
)

# Si substrate.
si_substrate = td.Structure(
    geometry=td.Box.from_bounds(
        rmin=(-eff_inf, -eff_inf, -eff_inf),
        rmax=(eff_inf, eff_inf, -w_thick / 2 - box_thick)
    ),
    medium=mat_si,
)

# Gaussian source focused above the grating coupler.
gauss_source = td.GaussianBeam(
    center=(dr_center_x, 0, src_pos_z),
    size=(dr_size_x, dr_size_y, 0),
    source_time=td.GaussianPulse(freq0=freq, fwidth=freqw),
    pol_angle=np.pi / 2,
    angle_theta=fiber_tilt * np.pi / 180.0,
    direction="-",
    num_freqs=7,
    waist_radius=spot_size / 2,
)

# Monitor where we will compute the objective function from.
mode_spec = td.ModeSpec(num_modes=1, target_neff=nSi)
fom_monitor = td.ModeMonitor(
    center=[mon_pos_x, 0, 0],
    size=[0, mon_w, mon_h],
    freqs=[freq],
    mode_spec=mode_spec,
    name=fom_name,
)

Now, we will define a random vector of initial design parameters or load a previously designed structure.

[6]:
init_par = np.random.uniform(0, 1, npar)

# Load parameters if continuing/visualizing a previous optimization:
if iter_to_do < total_iter:
    init_par = np.array(prev_data["design_parameters"])
    init_beta = prev_data["final_beta"]
# Only smooths the initial random distribution otherwise.
else:
    init_par = sp.ndimage.gaussian_filter(init_par, 1)

Fabricability Improvements#

We will use jax to build functions that improve device fabricability. A classical conic density filter, which is popular in topology optimization problems, is used to enforce a minimum feature size specified by the feature_size variable. Next, a hyperbolic tangent projection function is applied to eliminate grayscale and obtain a binarized permittivity pattern. The beta parameter controls the sharpness of the transition in the projection function, and for better results, this parameter should be gradually increased throughout the optimization process. Finally, the design parameters are transformed into permittivity values. For a detailed review of these methods, refer to [3].

[7]:
def get_eps(design_param, beta: float = 1.00, binarize: bool = False) -> np.ndarray:
    """Returns the permittivities after applying a conic density filter on design parameters
    to enforce fabrication constraints, followed by a binarization projection function
    which reduces grayscale.
    Parameters:
        design_param: np.ndarray
            Vector of design parameters.
        beta: float = 1.0
            Sharpness parameter for the projection filter.
        binarize: bool = False
            Enforce binarization.
    Returns:
        eps: np.ndarray
            Permittivity vector.
    """
    # Reshapes the design parameters into a 2D matrix.
    rho = np.reshape(design_param, (nx, ny))

    # Builds the conic filter and apply it to design parameters.
    filter_radius = np.ceil((feature_size * np.sqrt(3)) / dr_grid_size)
    xy = np.linspace(-filter_radius, filter_radius, int(2 * filter_radius + 1))
    xm, ym = np.meshgrid(xy, xy)
    xy_rad = np.sqrt(xm**2 + ym**2)
    kernel = jnp.where(filter_radius - xy_rad > 0, filter_radius - xy_rad, 0)
    filt_den = jsp.signal.convolve(jnp.ones_like(rho), kernel, mode="same")
    rho_dot = jsp.signal.convolve(rho, kernel, mode="same") / filt_den

    # Applies a hyperbolic tangent binarization function.
    rho_bar = (jnp.tanh(beta * eta) + jnp.tanh(beta * (rho_dot - eta))) / (
        jnp.tanh(beta * eta) + jnp.tanh(beta * (1.0 - eta))
    )

    # Calculates the permittivities from the transformed design parameters.
    eps = eps_min + (eps_max - eps_min) * rho_bar
    if binarize:
        eps = jnp.where(eps < (eps_min + eps_max) / 2, eps_min, eps_max)
    else:
        eps = jnp.where(eps < eps_min, eps_min, eps)
        eps = jnp.where(eps > eps_max, eps_max, eps)
    return eps

The permittivity values obtained from the design parameters are then used to build a JaxCustomMedium. As we will consider symmetry about the x-axis in the simulations, only the upper-half part of the design region needs to be populated. A JaxStructure built using the JaxCustomMedium will be returned by the following function:

[8]:
def update_design(eps, unfold: bool = False) -> List[JaxStructure]:
    # Reflects the structure about the x-axis.
    nyii = ny
    y_min = 0
    dr_s_y = dr_size_y / 2
    dr_c_y = dr_s_y / 2
    eps_val = jnp.array(eps).reshape((nx, ny, 1, 1))
    if unfold:
        nyii = 2 * ny
        y_min = -dr_size_y / 2
        dr_s_y = dr_size_y
        dr_c_y = 0
        eps_val = np.concatenate((np.fliplr(np.copy(eps_val)), eps_val), axis=1)

    # Definition of the coordinates x,y along the design region.
    coords_x = [(dr_center_x - dr_size_x / 2) + ix * dr_grid_size for ix in range(nx)]
    coords_y = [y_min + iy * dr_grid_size for iy in range(nyii)]
    coords = dict(x=coords_x, y=coords_y, z=[0], f=[freq])

    # Creation of a custom medium using the values of the design parameters.
    eps_components = {
        f"eps_{dim}{dim}": JaxDataArray(values=eps_val, coords=coords) for dim in "xyz"
    }
    eps_dataset = JaxPermittivityDataset(**eps_components)
    eps_medium = JaxCustomMedium(eps_dataset=eps_dataset)
    box = JaxBox(center=(dr_center_x, dr_c_y, 0), size=(dr_size_x, dr_s_y, w_thick))
    design_structure = JaxStructure(geometry=box, medium=eps_medium)
    return [design_structure]

Next, we will write a function to return the JaxSimulation object. Note that we are using a MeshOverrideStructure to obtain a uniform mesh over the design region.

[9]:
def make_adjoint_sim(
    design_param, beta: float = 1.00, unfold: bool = False, binarize: bool=False
) -> JaxSimulation:
    # Builds the design region from the design parameters.
    eps = get_eps(design_param, beta, binarize)
    design_structure = update_design(eps, unfold=unfold)

    # Creates a uniform mesh for the design region.
    adjoint_dr_mesh = td.MeshOverrideStructure(
        geometry=td.Box(
            center=(dr_center_x, 0, 0), size=(dr_size_x, dr_size_y, w_thick)
        ),
        dl=[dr_grid_size, dr_grid_size, dr_grid_size],
        enforce=True,
    )

    return JaxSimulation(
        size=[size_x, size_y, size_z],
        center=[0, 0, -center_z],
        grid_spec=td.GridSpec.auto(
            wavelength=wl_max,
            min_steps_per_wvl=15,
            override_structures=[adjoint_dr_mesh],
        ),
        symmetry=(0, -1, 0),
        structures=[waveguide, sio2_substrate, si_substrate],
        input_structures=design_structure,
        sources=[gauss_source],
        monitors=[],
        output_monitors=[fom_monitor],
        run_time=run_time,
        subpixel=True,
    )

Let’s visualize the simulation set up and verify if all the elements are in their correct places.

[10]:
init_design = make_adjoint_sim(init_par, beta=init_beta)

fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(10, 10))
init_design.plot_eps(z=0, ax=ax1)
init_design.plot_eps(y=0, ax=ax2)
plt.show()

WARNING:jax._src.lib.xla_bridge:No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
../_images/notebooks_AdjointPlugin6GratingCoupler_19_1.png

Optimization#

We need to provide an objective function and its gradients with respect to the design parameters of the optimization algorithm.

Our figure-of-merit (FOM) is the coupling efficiency of the incident power into the fundamental transverse electric mode of the \(Si\) waveguide. The optimization algorithm will call the objective function at each iteration step. Therefore, the objective function will create the adjoint simulation, run it, and return the FOM value.

[11]:
# Figure of Merit (FOM) calculation.
def fom(sim_data: JaxSimulationData) -> float:
    """Return the power at the mode index of interest."""
    output_amps = sim_data.output_data[0].amps
    amp = output_amps.sel(direction="-", f=freq, mode_index=0)
    return jnp.sum(jnp.abs(amp) ** 2)


# Objective function to be passed to the optimization algorithm.
def obj(
    design_param, beta: float = 1.0, step_num: int = None, verbose: bool = False
) -> float:
    sim = make_adjoint_sim(design_param, beta)
    task_name = "inv_des"
    if step_num:
        task_name += f"_step_{step_num}"
    sim_data = run(sim, task_name=task_name, verbose=verbose)
    fom_val = fom(sim_data)
    return fom_val

# Function to calculate the objective function value and its
# gradient with respect to the design parameters.
obj_grad = value_and_grad(obj)

Next, we will define an Optimizer class based on Adam gradient-descent algorithm to drive the inverse design process.

[12]:
class Optimizer(pd.BaseModel):
    class Config:
        arbitrary_types_allowed = True

    val_and_grad: Callable = None # optimization function
    parameters: np.ndarray # current state of the parameters

    # General optimizer.
    objective_history: list = []
    param_history: list = []
    num_it: int = 0 # current iteration number

class AdamOptimizer(Optimizer):
    # Adam specific.
    step_size: float = 0.02
    beta1: float = 0.9
    beta2: float = 0.999
    epsilon: float = 1e-8

    # Adam specific history parameters.
    mt: np.ndarray = None
    vt: np.ndarray = None
    mt_history: list = []
    vt_history: list = []

    def __init__(self,
                init_mt: np.ndarray=None,
                init_vt: np.ndarray=None,
                *args,
                **kwargs):

        super().__init__(*args, **kwargs)
        # Set mt and vt.
        self.mt = np.zeros_like(self.parameters) if init_mt is None else init_mt
        self.vt = np.zeros_like(self.parameters) if init_mt is None else init_vt

    def step(self, *args, **kwargs):
        """args and kwargs are optional extra arguments passed to self.val_and_grad"""

        val, grad = self.val_and_grad(self.parameters, *args, **kwargs)
        grad = np.array(grad).reshape(self.parameters.shape)

        # Display current status and update history variables.
        print(f"At iteration {self.num_it}")
        print(f"\tObjective function = {val:.4e}")
        print(f"\tGrad_norm = {np.linalg.norm(grad):.4e}")

        # Update Adam optimizer history.
        self.num_it += 1
        self.mt = self.beta1 * self.mt + (1 - self.beta1) * grad
        self.vt = self.beta2 * self.vt + (1 - self.beta2) * grad ** 2
        mt_hat = self.mt / (1 - self.beta1 ** self.num_it)
        vt_hat = self.vt / (1 - self.beta2 ** self.num_it)

        # Update parameters.
        update = self.step_size * mt_hat / (np.sqrt(vt_hat) + self.epsilon)
        self.parameters += update

        # Update history.
        self.objective_history.append(np.array(val))
        self.param_history.append(self.parameters.copy())
        self.mt_history.append(self.mt.copy())
        self.vt_history.append(self.vt.copy())

Initialize a new optimizer or load the state from a previous optimization.

[13]:
if prev_data:
    mt = np.array(prev_data["mt"])
    vt = np.array(prev_data["vt"])
    print("Restoring the previous optimizer state.")
else:
    mt = np.zeros_like(init_par)
    vt = np.zeros_like(init_par)
    print("Initializing a new optimizer.")

optimizer = AdamOptimizer(
    val_and_grad=obj_grad,
    parameters=init_par,
    init_mt=mt,
    init_vt=vt
)
Initializing a new optimizer.

Now, we are ready to run the optimization!

[14]:
td.config.logging_level='ERROR'

# Run a new optimization or load the previous results.
final_beta = init_beta
if iter_to_do == 0:
    final_par = init_par
    obj_vals = prev_data["obj_vals"]
else:
    for iteration in range(total_iter - iter_to_do, total_iter):
        step = iteration // it_per_step
        beta = step * del_beta + init_beta
        final_beta = beta
        print(f"Step {step}, beta = {beta}")
        optimizer.step(beta=beta)

        # Saves the optimization data into a json file.
        data = [
            {
                "design_parameters": optimizer.param_history[-1].tolist(),
                "final_beta": beta,
                "obj_vals": np.array(optimizer.objective_history).tolist(),
                "mt": optimizer.mt_history[-1].tolist(),
                "vt": optimizer.vt_history[-1].tolist(),
            }
        ]
        json_string = json.dumps(data)
        with open(output_file, "w", encoding="utf-8") as file_json:
            file_json.write(json_string)

    # Gets the final parameters and the objective values history.
    final_par = optimizer.param_history[-1] # note: not optimizer.parameters as this has been stepped already
    if prev_data:
        obj_vals = np.array(prev_data["obj_vals"] + optimizer.objective_history).ravel()
    else:
        obj_vals = np.array(optimizer.objective_history).ravel()
Step 0, beta = 10
At iteration 0
        Objective function = 7.8787e-05
        Grad_norm = 7.3158e-04
Step 0, beta = 10
At iteration 1
        Objective function = 4.8018e-02
        Grad_norm = 3.1627e-02
Step 0, beta = 10
At iteration 2
        Objective function = 1.6368e-01
        Grad_norm = 5.7410e-02
Step 0, beta = 10
At iteration 3
        Objective function = 3.0364e-01
        Grad_norm = 1.5769e-01
Step 0, beta = 10
At iteration 4
        Objective function = 3.6633e-01
        Grad_norm = 1.1269e-01
Step 0, beta = 10
At iteration 5
        Objective function = 4.6106e-01
        Grad_norm = 4.4629e-02
Step 0, beta = 10
At iteration 6
        Objective function = 4.8594e-01
        Grad_norm = 5.9591e-02
Step 0, beta = 10
At iteration 7
        Objective function = 5.1013e-01
        Grad_norm = 3.9100e-02
Step 0, beta = 10
At iteration 8
        Objective function = 5.3420e-01
        Grad_norm = 1.9559e-02
Step 0, beta = 10
At iteration 9
        Objective function = 5.5337e-01
        Grad_norm = 1.3574e-02
Step 1, beta = 20
At iteration 10
        Objective function = 3.9854e-01
        Grad_norm = 5.3562e-02
Step 1, beta = 20
At iteration 11
        Objective function = 5.2260e-01
        Grad_norm = 3.4804e-02
Step 1, beta = 20
At iteration 12
        Objective function = 5.4442e-01
        Grad_norm = 4.8332e-02
Step 1, beta = 20
At iteration 13
        Objective function = 5.2761e-01
        Grad_norm = 4.3154e-02
Step 1, beta = 20
At iteration 14
        Objective function = 5.4339e-01
        Grad_norm = 3.4879e-02
Step 1, beta = 20
At iteration 15
        Objective function = 5.8152e-01
        Grad_norm = 2.4981e-02
Step 1, beta = 20
At iteration 16
        Objective function = 5.8416e-01
        Grad_norm = 2.3618e-02
Step 1, beta = 20
At iteration 17
        Objective function = 5.8323e-01
        Grad_norm = 2.2622e-02
Step 1, beta = 20
At iteration 18
        Objective function = 5.8257e-01
        Grad_norm = 2.7430e-02
Step 1, beta = 20
At iteration 19
        Objective function = 5.9669e-01
        Grad_norm = 1.8816e-02
Step 2, beta = 30
At iteration 20
        Objective function = 5.8909e-01
        Grad_norm = 2.2657e-02
Step 2, beta = 30
At iteration 21
        Objective function = 5.9887e-01
        Grad_norm = 3.5007e-02
Step 2, beta = 30
At iteration 22
        Objective function = 5.9446e-01
        Grad_norm = 3.7143e-02
Step 2, beta = 30
At iteration 23
        Objective function = 6.0662e-01
        Grad_norm = 2.2223e-02
Step 2, beta = 30
At iteration 24
        Objective function = 6.1492e-01
        Grad_norm = 3.7760e-02
Step 2, beta = 30
At iteration 25
        Objective function = 6.2052e-01
        Grad_norm = 3.1330e-02
Step 2, beta = 30
At iteration 26
        Objective function = 6.2352e-01
        Grad_norm = 2.3044e-02
Step 2, beta = 30
At iteration 27
        Objective function = 6.2643e-01
        Grad_norm = 3.7923e-02
Step 2, beta = 30
At iteration 28
        Objective function = 6.3499e-01
        Grad_norm = 2.7934e-02
Step 2, beta = 30
At iteration 29
        Objective function = 6.4088e-01
        Grad_norm = 2.1197e-02
Step 3, beta = 40
At iteration 30
        Objective function = 6.2578e-01
        Grad_norm = 6.6360e-02
Step 3, beta = 40
At iteration 31
        Objective function = 6.2692e-01
        Grad_norm = 6.8806e-02
Step 3, beta = 40
At iteration 32
        Objective function = 6.4573e-01
        Grad_norm = 1.6920e-02
Step 3, beta = 40
At iteration 33
        Objective function = 6.4124e-01
        Grad_norm = 5.7398e-02
Step 3, beta = 40
At iteration 34
        Objective function = 6.4168e-01
        Grad_norm = 6.9137e-02
Step 3, beta = 40
At iteration 35
        Objective function = 6.5455e-01
        Grad_norm = 1.8951e-02
Step 3, beta = 40
At iteration 36
        Objective function = 6.4892e-01
        Grad_norm = 6.0518e-02
Step 3, beta = 40
At iteration 37
        Objective function = 6.5222e-01
        Grad_norm = 5.5811e-02
Step 3, beta = 40
At iteration 38
        Objective function = 6.6217e-01
        Grad_norm = 1.3080e-02
Step 3, beta = 40
At iteration 39
        Objective function = 6.5283e-01
        Grad_norm = 6.4406e-02
Step 4, beta = 50
At iteration 40
        Objective function = 6.6467e-01
        Grad_norm = 1.4755e-02
Step 4, beta = 50
At iteration 41
        Objective function = 6.5750e-01
        Grad_norm = 6.4817e-02
Step 4, beta = 50
At iteration 42
        Objective function = 6.5981e-01
        Grad_norm = 6.3940e-02
Step 4, beta = 50
At iteration 43
        Objective function = 6.6727e-01
        Grad_norm = 3.3743e-02
Step 4, beta = 50
At iteration 44
        Objective function = 6.6944e-01
        Grad_norm = 4.2831e-02
Step 4, beta = 50
At iteration 45
        Objective function = 6.7020e-01
        Grad_norm = 3.2677e-02
Step 4, beta = 50
At iteration 46
        Objective function = 6.7406e-01
        Grad_norm = 2.7373e-02
Step 4, beta = 50
At iteration 47
        Objective function = 6.7144e-01
        Grad_norm = 3.0082e-02
Step 4, beta = 50
At iteration 48
        Objective function = 6.7817e-01
        Grad_norm = 1.1559e-02
Step 4, beta = 50
At iteration 49
        Objective function = 6.7394e-01
        Grad_norm = 3.7587e-02
Step 5, beta = 60
At iteration 50
        Objective function = 6.7873e-01
        Grad_norm = 1.6116e-02
Step 5, beta = 60
At iteration 51
        Objective function = 6.7827e-01
        Grad_norm = 2.8026e-02
Step 5, beta = 60
At iteration 52
        Objective function = 6.7681e-01
        Grad_norm = 3.5310e-02
Step 5, beta = 60
At iteration 53
        Objective function = 6.8095e-01
        Grad_norm = 1.6904e-02
Step 5, beta = 60
At iteration 54
        Objective function = 6.8135e-01
        Grad_norm = 2.1578e-02
Step 5, beta = 60
At iteration 55
        Objective function = 6.8237e-01
        Grad_norm = 2.9715e-02
Step 5, beta = 60
At iteration 56
        Objective function = 6.8458e-01
        Grad_norm = 1.3201e-02
Step 5, beta = 60
At iteration 57
        Objective function = 6.8457e-01
        Grad_norm = 2.6202e-02
Step 5, beta = 60
At iteration 58
        Objective function = 6.8622e-01
        Grad_norm = 2.1887e-02
Step 5, beta = 60
At iteration 59
        Objective function = 6.8759e-01
        Grad_norm = 1.4890e-02
Step 6, beta = 70
At iteration 60
        Objective function = 6.8776e-01
        Grad_norm = 1.8331e-02
Step 6, beta = 70
At iteration 61
        Objective function = 6.8853e-01
        Grad_norm = 1.1289e-02
Step 6, beta = 70
At iteration 62
        Objective function = 6.8889e-01
        Grad_norm = 1.7278e-02
Step 6, beta = 70
At iteration 63
        Objective function = 6.8997e-01
        Grad_norm = 1.4952e-02
Step 6, beta = 70
At iteration 64
        Objective function = 6.9056e-01
        Grad_norm = 1.3550e-02
Step 6, beta = 70
At iteration 65
        Objective function = 6.9190e-01
        Grad_norm = 1.0412e-02
Step 6, beta = 70
At iteration 66
        Objective function = 6.9186e-01
        Grad_norm = 1.3028e-02
Step 6, beta = 70
At iteration 67
        Objective function = 6.9353e-01
        Grad_norm = 5.8804e-03
Step 6, beta = 70
At iteration 68
        Objective function = 6.9393e-01
        Grad_norm = 9.3477e-03
Step 6, beta = 70
At iteration 69
        Objective function = 6.9438e-01
        Grad_norm = 1.2194e-02
Step 7, beta = 80
At iteration 70
        Objective function = 6.9466e-01
        Grad_norm = 1.8469e-02
Step 7, beta = 80
At iteration 71
        Objective function = 6.9380e-01
        Grad_norm = 2.6154e-02
Step 7, beta = 80
At iteration 72
        Objective function = 6.9387e-01
        Grad_norm = 2.3957e-02
Step 7, beta = 80
At iteration 73
        Objective function = 6.9242e-01
        Grad_norm = 3.4005e-02
Step 7, beta = 80
At iteration 74
        Objective function = 6.9083e-01
        Grad_norm = 4.0479e-02
Step 7, beta = 80
At iteration 75
        Objective function = 6.9177e-01
        Grad_norm = 4.6328e-02
Step 7, beta = 80
At iteration 76
        Objective function = 6.8915e-01
        Grad_norm = 5.8739e-02
Step 7, beta = 80
At iteration 77
        Objective function = 6.9357e-01
        Grad_norm = 4.5696e-02
Step 7, beta = 80
At iteration 78
        Objective function = 6.9759e-01
        Grad_norm = 2.0101e-02
Step 7, beta = 80
At iteration 79
        Objective function = 6.9816e-01
        Grad_norm = 3.0047e-02
Step 8, beta = 90
At iteration 80
        Objective function = 6.9263e-01
        Grad_norm = 4.5487e-02
Step 8, beta = 90
At iteration 81
        Objective function = 6.9537e-01
        Grad_norm = 4.0683e-02
Step 8, beta = 90
At iteration 82
        Objective function = 6.9924e-01
        Grad_norm = 2.7934e-02
Step 8, beta = 90
At iteration 83
        Objective function = 6.9956e-01
        Grad_norm = 2.1489e-02
Step 8, beta = 90
At iteration 84
        Objective function = 6.9826e-01
        Grad_norm = 4.0911e-02
Step 8, beta = 90
At iteration 85
        Objective function = 6.9955e-01
        Grad_norm = 3.1945e-02
Step 8, beta = 90
At iteration 86
        Objective function = 7.0155e-01
        Grad_norm = 2.1972e-02
Step 8, beta = 90
At iteration 87
        Objective function = 6.9874e-01
        Grad_norm = 3.8842e-02
Step 8, beta = 90
At iteration 88
        Objective function = 7.0086e-01
        Grad_norm = 3.0929e-02
Step 8, beta = 90
At iteration 89
        Objective function = 7.0276e-01
        Grad_norm = 1.7881e-02
Step 9, beta = 100
At iteration 90
        Objective function = 7.0241e-01
        Grad_norm = 3.2847e-02
Step 9, beta = 100
At iteration 91
        Objective function = 6.9862e-01
        Grad_norm = 4.5569e-02
Step 9, beta = 100
At iteration 92
        Objective function = 7.0254e-01
        Grad_norm = 3.0195e-02
Step 9, beta = 100
At iteration 93
        Objective function = 7.0349e-01
        Grad_norm = 2.5936e-02
Step 9, beta = 100
At iteration 94
        Objective function = 6.9935e-01
        Grad_norm = 4.3035e-02
Step 9, beta = 100
At iteration 95
        Objective function = 7.0155e-01
        Grad_norm = 3.6590e-02
Step 9, beta = 100
At iteration 96
        Objective function = 7.0097e-01
        Grad_norm = 4.0512e-02
Step 9, beta = 100
At iteration 97
        Objective function = 6.9925e-01
        Grad_norm = 5.6556e-02
Step 9, beta = 100
At iteration 98
        Objective function = 7.0168e-01
        Grad_norm = 4.2130e-02
Step 9, beta = 100
At iteration 99
        Objective function = 7.0247e-01
        Grad_norm = 3.6289e-02
Step 10, beta = 110
At iteration 100
        Objective function = 7.0552e-01
        Grad_norm = 2.3682e-02
Step 10, beta = 110
At iteration 101
        Objective function = 7.0666e-01
        Grad_norm = 1.4257e-02
Step 10, beta = 110
At iteration 102
        Objective function = 7.0521e-01
        Grad_norm = 3.0919e-02
Step 10, beta = 110
At iteration 103
        Objective function = 7.0437e-01
        Grad_norm = 3.4261e-02
Step 10, beta = 110
At iteration 104
        Objective function = 7.0451e-01
        Grad_norm = 3.7208e-02
Step 10, beta = 110
At iteration 105
        Objective function = 7.0310e-01
        Grad_norm = 4.5987e-02
Step 10, beta = 110
At iteration 106
        Objective function = 7.0444e-01
        Grad_norm = 4.5127e-02
Step 10, beta = 110
At iteration 107
        Objective function = 7.0541e-01
        Grad_norm = 3.7913e-02
Step 10, beta = 110
At iteration 108
        Objective function = 7.0637e-01
        Grad_norm = 3.1812e-02
Step 10, beta = 110
At iteration 109
        Objective function = 7.0838e-01
        Grad_norm = 2.4365e-02
Step 11, beta = 120
At iteration 110
        Objective function = 7.0941e-01
        Grad_norm = 1.5828e-02
Step 11, beta = 120
At iteration 111
        Objective function = 7.0999e-01
        Grad_norm = 1.1399e-02
Step 11, beta = 120
At iteration 112
        Objective function = 7.1044e-01
        Grad_norm = 1.3538e-02
Step 11, beta = 120
At iteration 113
        Objective function = 7.1036e-01
        Grad_norm = 1.1503e-02
Step 11, beta = 120
At iteration 114
        Objective function = 7.1106e-01
        Grad_norm = 1.3215e-02
Step 11, beta = 120
At iteration 115
        Objective function = 7.0978e-01
        Grad_norm = 2.4962e-02
Step 11, beta = 120
At iteration 116
        Objective function = 7.0811e-01
        Grad_norm = 3.9985e-02
Step 11, beta = 120
At iteration 117
        Objective function = 7.0220e-01
        Grad_norm = 6.0520e-02
Step 11, beta = 120
At iteration 118
        Objective function = 6.9057e-01
        Grad_norm = 8.7708e-02
Step 11, beta = 120
At iteration 119
        Objective function = 6.8557e-01
        Grad_norm = 9.6025e-02
Step 12, beta = 130
At iteration 120
        Objective function = 7.0355e-01
        Grad_norm = 5.1987e-02
Step 12, beta = 130
At iteration 121
        Objective function = 7.0868e-01
        Grad_norm = 3.1631e-02
Step 12, beta = 130
At iteration 122
        Objective function = 6.9763e-01
        Grad_norm = 6.5204e-02
Step 12, beta = 130
At iteration 123
        Objective function = 7.1038e-01
        Grad_norm = 2.0649e-02
Step 12, beta = 130
At iteration 124
        Objective function = 7.0522e-01
        Grad_norm = 4.4686e-02
Step 12, beta = 130
At iteration 125
        Objective function = 7.0632e-01
        Grad_norm = 3.8831e-02
Step 12, beta = 130
At iteration 126
        Objective function = 7.1051e-01
        Grad_norm = 2.4704e-02
Step 12, beta = 130
At iteration 127
        Objective function = 7.0595e-01
        Grad_norm = 4.6373e-02
Step 12, beta = 130
At iteration 128
        Objective function = 7.1229e-01
        Grad_norm = 6.4697e-03
Step 12, beta = 130
At iteration 129
        Objective function = 7.0779e-01
        Grad_norm = 3.5093e-02
Step 13, beta = 140
At iteration 130
        Objective function = 7.1229e-01
        Grad_norm = 8.1188e-03
Step 13, beta = 140
At iteration 131
        Objective function = 7.0907e-01
        Grad_norm = 3.3656e-02
Step 13, beta = 140
At iteration 132
        Objective function = 7.1239e-01
        Grad_norm = 1.7798e-02
Step 13, beta = 140
At iteration 133
        Objective function = 7.1129e-01
        Grad_norm = 2.4917e-02
Step 13, beta = 140
At iteration 134
        Objective function = 7.1160e-01
        Grad_norm = 2.4725e-02
Step 13, beta = 140
At iteration 135
        Objective function = 7.1328e-01
        Grad_norm = 1.3879e-02
Step 13, beta = 140
At iteration 136
        Objective function = 7.1158e-01
        Grad_norm = 2.6793e-02
Step 13, beta = 140
At iteration 137
        Objective function = 7.1391e-01
        Grad_norm = 5.4024e-03
Step 13, beta = 140
At iteration 138
        Objective function = 7.1211e-01
        Grad_norm = 2.5809e-02
Step 13, beta = 140
At iteration 139
        Objective function = 7.1435e-01
        Grad_norm = 7.9815e-03
Step 14, beta = 150
At iteration 140
        Objective function = 7.1318e-01
        Grad_norm = 2.0542e-02
Step 14, beta = 150
At iteration 141
        Objective function = 7.1413e-01
        Grad_norm = 1.5115e-02
Step 14, beta = 150
At iteration 142
        Objective function = 7.1408e-01
        Grad_norm = 1.4412e-02
Step 14, beta = 150
At iteration 143
        Objective function = 7.1402e-01
        Grad_norm = 1.8037e-02
Step 14, beta = 150
At iteration 144
        Objective function = 7.1497e-01
        Grad_norm = 1.0758e-02
Step 14, beta = 150
At iteration 145
        Objective function = 7.1437e-01
        Grad_norm = 1.5867e-02
Step 14, beta = 150
At iteration 146
        Objective function = 7.1539e-01
        Grad_norm = 3.4644e-03
Step 14, beta = 150
At iteration 147
        Objective function = 7.1483e-01
        Grad_norm = 1.5382e-02
Step 14, beta = 150
At iteration 148
        Objective function = 7.1564e-01
        Grad_norm = 8.0472e-03
Step 14, beta = 150
At iteration 149
        Objective function = 7.1526e-01
        Grad_norm = 1.3292e-02

Optimization Results#

After 150 iterations, a coupling efficiency value of 0.71 (-1.48 dB) was achieved at the central wavelength.

[15]:
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
ax.plot(obj_vals, "ro")
ax.set_xlabel("iterations")
ax.set_ylabel("objective function")
ax.set_ylim(0, 1)
ax.set_title(f"Final Objective Function Value: {obj_vals[-1]:.2f}")
plt.show()

../_images/notebooks_AdjointPlugin6GratingCoupler_29_0.png

The final grating coupler structure is well binarized, with mostly black (eps_max) and white (eps_min) regions.

[20]:
fig, ax = plt.subplots(1, figsize=(4, 4))
sim_final = make_adjoint_sim(final_par, beta=final_beta, unfold=True)
sim_final = sim_final.to_simulation()[0]
sim_final.plot_eps(z=0, source_alpha=0, monitor_alpha=0, ax=ax)
plt.show()

../_images/notebooks_AdjointPlugin6GratingCoupler_31_0.png

Once the inverse design is complete, we can visualize the field distributions and the wavelength dependent coupling efficiency.

[17]:
# Field monitors to visualize the final fields.
field_xy = td.FieldMonitor(
    size=(td.inf, td.inf, 0),
    freqs=[freq],
    name="field_xy",
)

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

# Monitor to compute the grating coupler efficiency.
gc_efficiency = td.ModeMonitor(
    center=[mon_pos_x, 0, 0],
    size=[0, mon_w, mon_h],
    freqs=freqs,
    mode_spec=mode_spec,
    name="gc_efficiency",
)

sim_final = sim_final.copy(update=dict(monitors=(field_xy, field_xz, gc_efficiency)))
sim_data_final = web.run(sim_final, task_name="inv_des_final")
[23:19:10] Created task 'inv_des_final' with task_id 'fdve-4d076c97-fb0c-49c2-933e-ef54b3735265v1'.   webapi.py:187
[23:19:16] status = queued                                                                            webapi.py:322
[23:19:25] status = preprocess                                                                        webapi.py:316
[23:19:32] Maximum FlexCredit cost: 0.183. Use 'web.real_cost(task_id)' to get the billed FlexCredit  webapi.py:339
           cost after a simulation run.                                                                            
           starting up solver                                                                         webapi.py:343
           running solver                                                                             webapi.py:353
[23:20:06] early shutoff detected, exiting.                                                           webapi.py:367
[23:20:07] status = postprocess                                                                       webapi.py:384
[23:20:15] status = success                                                                           webapi.py:391
[23:20:24] loading SimulationData from simulation_data.hdf5                                           webapi.py:569
[18]:
mode_amps = sim_data_final["gc_efficiency"]
coeffs_f = mode_amps.amps.sel(direction="-")
power_0 = np.abs(coeffs_f.sel(mode_index=0)) ** 2
power_0_db = 10 * np.log10(power_0)

sim_plot = sim_final.updated_copy(symmetry=(0, 0, 0), monitors=(field_xy, field_xz, gc_efficiency))
sim_data_plot = sim_data_final.updated_copy(simulation=sim_plot)

f, ax = plt.subplots(2, 2, figsize=(8, 6), tight_layout=True)
sim_plot.plot_eps(z=0, source_alpha=0, monitor_alpha=0, ax=ax[0, 1])
ax[1, 0].plot(wl_range, power_0_db, "-k")
ax[1, 0].set_xlabel("Wavelength (um)")
ax[1, 0].set_ylabel("Power (dB)")
ax[1, 0].set_ylim(-15, 0)
ax[1, 0].set_xlim(wl - bw / 2, wl + bw / 2)
ax[1, 0].set_title("Coupling Efficiency")
sim_data_plot.plot_field("field_xy", "E", "abs^2", z=0, ax=ax[1, 1])
ax[0, 0].plot(obj_vals, "ro")
ax[0, 0].set_xlabel("iterations")
ax[0, 0].set_ylabel("objective function")
ax[0, 0].set_ylim(0, 1)
ax[0, 0].set_title(f"Final Objective Function Value: {obj_vals[-1]:.2f}")
plt.show()
../_images/notebooks_AdjointPlugin6GratingCoupler_34_0.png