This notebook contains a long optimization. Running the entire notebook will cost about 10 FlexCredits and take a few 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 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.
In addition, if you are interested in more conventional designs, we modeled an uniform grating coupler and a Focusing apodized grating coupler in previous case studies. For more integrated photonic examples, please visit our examples page. If you are new to the finite-difference time-domain (FDTD) method, we highly recommend going through our FDTD101 tutorials. FDTD simulations can diverge due to various reasons. If you run into any simulation divergence issues, please follow the steps outlined in our troubleshooting guide to resolve it.
We start by importing our typical python packages, plus autograd and tidy3d.
# Standard python imports.from typing import List# Import autograd to be able to use automatic differentiation.import autograd.numpy as anpimport matplotlib.pylab as pltimport numpy as npimport scipy as sp# Import regular tidy3d.import tidy3d as tdimport tidy3d.web as webfrom autograd import value_and_grad
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 below.
# 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 setup parameters.################################################################## Total number of iterations = opt_steps x it_per_step.it_per_step =1# Number of iterations per optimization step.opt_steps =75# Number of optimization steps.#################################################################eta =0.50# Threshold value for the projection filter.fom_name ="fom_field"# Name of the monitor used to compute the objective function.# 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.# feature sizemin_feature_size =0.080filter_radius = min_feature_size# Buffer layer thicknessborder_buffer =0.16# projectionbeta_min =1.0beta_max =30.0
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.
We will use the tidy3d.plugins.autograd plugin to introduce 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 filter_radius 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].
We will also introduce a buffer layer around the design region to enhance fabricability at the interfaces. The permittivity is enforced to lower values within the buffer layer, except at the output waveguide connection where we want a smooth transition.
def get_eps(design_param: np.ndarray, 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. """# Calculates the permittivities from the transformed design parameters. eps = get_eps_values(design_param, beta=beta)if binarize: eps = anp.where(eps < (eps_min + eps_max) /2, eps_min, eps_max)else: eps = anp.where(eps < eps_min, eps_min, eps) eps = anp.where(eps > eps_max, eps_max, eps)return eps
from tidy3d.plugins.autograd import make_filter_and_project, rescalefilter_project = make_filter_and_project(filter_radius, dr_grid_size, padding="constant")def interface_buffer(params):"""Introduce a buffer around design to enhance fabricability at the interfaces.""" mask = anp.zeros_like(params) mask[0:n_border, :] =0 mask[nx - n_border :, :] =0 mask[:, ny - n_border :] =0 mask[0:n_border, 0 : int((w_width /2) / dr_grid_size) +1] =1return params * (1- mask) + maskdef pre_process(params, beta):"""Get the permittivity values (1, eps_wg) array as a function of the parameters (0,1)""" params1 = interface_buffer(params) params2 = filter_project(params1, beta=beta) params3 = filter_project(params2, beta=beta)return params3def get_eps_values(params: np.ndarray, beta: float) -> np.ndarray:"""Get the relative permittivity array given the parameters.""" params = pre_process(params, beta=beta) eps_values = rescale(params, eps_min, eps_max)return eps_values
The permittivity values obtained from the design parameters are then used to build a CustomMedium. 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 Structure built using the CustomMedium will be returned by the following function:
def update_design(eps, unfold: bool=False) -> List[td.Structure]:"""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 = anp.array(eps).reshape((nx, ny, 1))if unfold: nyii =2* ny y_min =-dr_size_y /2 dr_s_y = dr_size_y dr_c_y =0 eps_val = anp.concatenate((anp.fliplr(anp.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 inrange(nx)] coords_y = [y_min + iy * dr_grid_size for iy inrange(nyii)] coords =dict(x=coords_x, y=coords_y, z=[0])# Creation of a custom medium using the values of the design parameters. permittivity = td.SpatialDataArray(eps_val, coords=coords) eps_medium = td.CustomMedium(permittivity=permittivity) box = td.Box(center=(dr_center_x, dr_c_y, 0), size=(dr_size_x, dr_s_y, w_thick)) design_structure = td.Structure(geometry=box, medium=eps_medium)return [design_structure]
Next, we will write a function to return the td.Simulation object. Note that we are using a MeshOverrideStructure to obtain a uniform mesh over the design region.
from tidy3d.plugins.autograd import make_erosion_dilation_penaltyerode_dilate_penalty = make_erosion_dilation_penalty(filter_radius, dr_grid_size)# Figure of Merit (FOM) calculation.def fom(sim_data: td.SimulationData) ->float:"""Return the power at the mode index of interest.""" output_amps = sim_data[fom_name].amps amp = output_amps.sel(direction="-", f=freq, mode_index=0).valuesreturn anp.sum(anp.abs(amp) **2)def penalty(params, beta) ->float:"""Penalty function based on amount of change in parameters after erosion and dilation.""" params_processed = pre_process(params, beta=beta)return erode_dilate_penalty(params_processed)# 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 = web.run(sim, task_name=task_name, verbose=verbose) fom_val = fom(sim_data) feature_size_penalty = penalty(design_param, beta=beta) J = fom_val - feature_size_penaltyreturn J# Function to calculate the objective function value and its# gradient with respect to the design parameters.obj_grad = value_and_grad(obj)
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.
Next we will define the optimizer using Tidy3D’s built-in Adam helper. We will save the optimization progress in a pickle file. If that file is found, it will pick up the optimization from the last state. Otherwise, we will create a blank history.
import picklefrom tidy3d.plugins.autograd import adam, apply_updates# hyperparameterslearning_rate =0.2optimizer = adam(learning_rate=learning_rate)# where to store historyhistory_fname ="misc/grating_coupler_history_autograd.pkl"def save_history(history_dict: dict) ->None:"""Convenience function to save the history to file."""withopen(history_fname, "wb") asfile: pickle.dump(history_dict, file)def load_history() ->dict:"""Convenience method to load the history from file."""withopen(history_fname, "rb") asfile: history_dict = pickle.load(file)return history_dict
Checking For a Previous Optimization
If history_fname 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 pickle file used in this notebook can be downloaded from our documentation repo.
iter_done =len(history_dict["values"])for i inrange(iter_done, total_iter):print(f"iteration = ({i +1} / {total_iter})")# compute gradient and current objective function value perc_done = i / (total_iter -1) beta_i = beta_min * (1- perc_done) + beta_max * perc_done value, gradient = obj_grad(params, beta=beta_i)# outputsprint(f"\tbeta = {beta_i}")print(f"\tJ = {value:.4e}")print(f"\tgrad_norm = {np.linalg.norm(gradient):.4e}")# compute and apply updates to the optimizer based on gradient (-1 sign to maximize obj_fn) updates, opt_state = optimizer.update(-gradient, opt_state, params) params[:] = apply_updates(params, updates)# cap parameters between 0 and 1 np.clip(params, 0.0, 1.0, out=params)# save history history_dict["values"].append(value) history_dict["params"].append(params) history_dict["beta"].append(beta_i) history_dict["gradients"].append(gradient) history_dict["opt_states"].append(opt_state)# history_dict["data"].append(sim_data_i) # uncomment to store data, can create large files save_history(history_dict)
09:31:41 UTC Estimated FlexCredit cost: 0.212. 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.
09:31:42 UTC status = queued
To cancel the simulation, use 'web.abort(task_id)' or
'web.delete(task_id)' or abort/delete the task in the web UI.
Terminating the Python script will not stop the job running on the
cloud.
09:31:50 UTC status = preprocess
09:31:54 UTC starting up solver
running solver
09:32:06 UTC early shutoff detected at 12%, exiting.
loss_db =max(power_0_db)print(f"optimized loss of {loss_db:.2f} dB")
optimized loss of -2.20 dB
Export to GDS
The Simulation object has the .to_gds_file convenience function to export the final design to a GDS file. In addition to a file name, it is necessary to set a cross-sectional plane (z = 0 in this case) on which to evaluate the geometry, a frequency to evaluate the permittivity, and a permittivity_threshold to define the shape boundaries in custom mediums. See the GDS export notebook for a detailed example on using .to_gds_file and other GDS related functions.
We use necessary cookies to run this website. With your permission, we
also use analytics cookies to understand site usage and marketing
cookies for advertising, retargeting, and HubSpot tracking.
Learn more about our cookie policy.
Privacy choices
Choose which optional cookies Flexcompute may use. Necessary cookies
are always on because they support core website behavior, security, and
saving your consent record.
Your browser is sending a Global Privacy Control signal, so marketing
cookies are disabled.
Subscribe
Thanks for subscribing
Publish Your Notebook
Thank you for publishingA confirmation email has been sent to your inbox. Your notebook will be available within the next 48 hours.
Community Library
TERMS & CONDITIONS
EFFECTIVE DATE: January 1, 2025
Terms and Conditions for User-Submitted Content
By submitting content to Flexcompute, you agree to the following terms:
1. Ownership and Copyright
All content, including but not limited to text, data, images, and other materials submitted by users,
remains the sole property of the original creator. Flexcompute does not claim ownership of the submitted
content or any intellectual property rights associated with it.
Users affirm that they own the copyright or have obtained all necessary permissions for the submitted
content and are fully responsible for ensuring that their submissions do not infringe on any third-party
rights.
2. Responsibility for Content
Users are solely responsible for the content they submit. Flexcompute does not endorse, guarantee,
or verify the accuracy, legality, or appropriateness of any submitted content. Users are responsible
for ensuring that their content complies with all applicable laws and regulations.
Users declare that they have obtained authorization from all co-authors and collaborators associated
with the submitted content, granting them the right to submit and sign on behalf of all contributors.
Users agree not to submit content that is illegal, defamatory, obscene, or violates the rights
of others, including privacy and intellectual property rights.
3. Modification and Presentation
Flexcompute reserves the right to review, edit, or modify submitted content to improve clarity,
formatting, and presentation while maintaining the original intent and message.
These modifications are made to enhance the overall quality and readability of the published material.
Users acknowledge that Flexcompute may format or present the content in a way
that aligns with our editorial and visual standards.
4. Liability Disclaimer
Flexcompute shall not be held liable for any disputes arising from user-submitted content, including
but not limited to copyright claims, inaccuracies, or damages resulting from the publication of user content.
Users agree to indemnify Flexcompute against any claims or legal actions resulting from their submitted content.
5. Content Use Rights
By submitting content, users grant Flexcompute a non-exclusive, royalty-free, worldwide license to publish, distribute,
and promote the content for the purposes of showcasing user contributions and marketing our services.
This license does not transfer ownership of the content or any copyright to Flexcompute.
Users retain the right to withdraw their submissions at any time.
Upon request, Flexcompute will remove the content from its platform.
6. Privacy and Confidentiality
Users acknowledge that submitted content may be publicly accessible and therefore waive any
rights to confidentiality or privacy regarding the published material.
Flexcompute will not share personal information of users without
their consent, in accordance with our privacy policy.
Community Library
How the process works:
You submit your notebook.
You will get a permanent URL to promote your work.
Cite the URL as a reference for others in your upcoming papers to enhance the impact.
When someone clicks your URL, your notebook will be displayed.
Enter your email address below to receive the presentation slides. In the future, we’ll share you very few emails when we have new tutorial release, development updates, valuable toolkits and technical guidance . You can unsubscribe at any time by clicking the link at the bottom of every email. We’ll never share your information.