In this example, we will walk you through performing a simple inverse design optimization of a 3D waveguide crossing.
In this example, we’ll be using a pixelated material grid to define the design region. However, one could also use shape parameterization to solve the same problem. You can see how this is done for a waveguide bend here.
Before using Tidy3D, you must first sign up for a user account. See this link for installation instructions.
In this tutorial we are demonstrating the smoothed projection by Alec Hammond et al. from the paper “Unifying and accelerating level-set and density-based topology optimization by subpixel-smoothed projection”. You can find the article here.
Setup
First we import the packages we need and also define some of the global parameters that define our problem.
Important note: we use autograd.numpy instead of regular numpy, this allows numpy functions to be differentiable. If one forgets this step, the error may be a bit opaque, just a heads up.
import mathimport autograd.numpy as npimport matplotlib.pylab as pltimport tidy3d as tdimport tidy3d.web as webfrom tidy3d import configfrom tidy3d.plugins.autograd import adam, optimizefrom tidy3d.plugins.autograd.invdes.filters import ConicFilterfrom tidy3d.plugins.autograd.invdes.parametrizations import initialize_params_from_simulationfrom tidy3d.plugins.autograd.invdes.projections import smoothed_projectionfrom tidy3d.plugins.autograd.invdes.symmetries import symmetrize_diagonal, symmetrize_mirrorconfig.simulation.use_local_subpixel =False
Next, we will define all the components that make up our “base” simulation. This simulation defines the static portion of our optimization, which doesn’t change over the iterations. Since our simulation scene has a symmetry in the y and z-axis, we will include the corresponding symmetry flag in the Simulation definition. This makes the simulation four times as fast and also reduces cost by a factor of four. For a tutorial on symmetries in simulations check out this notebook here.
For now, we’ll include a definition of the design region geometry, just to have that on hand later, but will not include a design region in the base simulation as we’ll add it later.
Let’s visualize the base simulations to verify that they look correct. The shaded region indicates that we are using a symmetry condition to determine the fields in this region, such that this region does not need to be simulated.
Next, we will define how our design region is constructed as a function of our optimization parameters.
We will define a structure containing a grid of permittivity values defined by an array. Since we have a symmetric waveguide crossing, we only need to optimize 1/8 of the design volume. Therefore, we use some symmetry helper functions to constrain our device to these symmetries.
We will convolve our optimization parameters with a conic filter to smooth the features over a given radius. Then we will add a smoothed projection function (see the paper here for full details). The advantage of this projection is that we can work with fully binarized structures in our simulation. Since we assume that our topology should not change during optimization, we can use a value of \(\beta = \infty\) during the whole simulation process, which completely binarizes the design except for a small boundary at the interface change. If we want to change the topology during optimization, we could also add a scheduling for the beta parameter, increasing it stepwise towards \(\beta = \infty\). However, for our example this is not necessary.
For more details on the parameterization process, we highly recommend our short tutorial, which explains the process in more detail.
def get_density(params: np.ndarray) -> np.ndarray:"""Get the density of the material in the design region as a function of optimization parameters.""" arr = symmetrize_mirror(params, axis=(0, 1)) arr = symmetrize_diagonal(arr)filter= ConicFilter(kernel_size=discrete_radius) arr_filtered =filter(arr) arr_projected = smoothed_projection(arr_filtered, beta=np.inf, eta=0.5)return arr_projecteddef get_design_region(params: np.ndarray) -> td.Structure:"""Get design region structure as a function of optimization parameters.""" density = np.clip(get_density(params), 0.0, 1.0) eps_data =1+ (eps_mat -1) * density[:, :, None]return td.Structure.from_permittivity_array(eps_data=eps_data, geometry=design_region_geometry)
Next, it is very convenient to wrap this in a function that returns an updated copy of the base simulation with the design region added. We’ll be calling this in our objective function. We’ll also add some logic to exclude field monitors if they aren’t needed, for example during the optimization.
def get_sim(params: np.ndarray, with_fld_mnt: bool=False) -> td.Simulation:"""Get simulation as a function of optimization parameters.""" design_region = get_design_region(params) sim = sim_base.updated_copy(structures=sim_base.structures + (design_region,))if with_fld_mnt: sim = sim.updated_copy( monitors=sim_base.monitors + (field_monitor, mode_monitor_multiple_wl) )return sim
To start with the optimization, we need some initial parameters that can be optimized. These initial parameters could be chosen randomly, but the optimization will run faster and need less iterations to converge if we start from a design that is already performing a little better than random. In this case it is very easy to come up with such a design: We just initialize our parameters such that they represent the waveguides of the base simulations, which we defined above. In other words, we initialize our parameters such that they represent a naive waveguide crossing where the two waveguides simply overlap.
However, it can be a bit difficult to work out how the parameters should look to form this structure in the simulation since there is a smoothing and projection involved. Luckily, we have a helper function, which automatically determines how the parameters should look to represent the base simulation. This helper function takes the desired simulation as input and optimizes our initial parameters to reflect this simulation as best as possible.
08:22:12 UTC WARNING: Design coordinates do not include the geometry center (x=0, y=0). Centered features may appear half-cell shifted or kinked when combined with symmetry or projection constraints. If you need a design pixel centered on the geometry center, use an oddnumber of pixels along those axes.
Let’s take a look at the resulting parameters which form the initial design. The raw parameters look noisy, but due to the conic filter and smoothed projection the parameters used in simulations form the initial cross design.
We can also run a quick simulation with a field monitor added to verify how poorly the initial device is transmitting. This gives us lots of room to improve things through optimization!
08:22:20 UTC Estimated FlexCredit cost: 0.298. 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.
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.
08:22:30 UTC status = preprocess
08:22:35 UTC starting up solver
running solver
08:22:42 UTC early shutoff detected at 3%, exiting.
The next step is to define the metric that we want to optimize, or our “figure of merit”. We will first write a function to compute the transmission (between 0-1) of the output mode given the simulation data.
def get_transmission_from_data(data): mode_amps = data["mode"].amps.sel(direction="+").values energy = np.sum(np.abs(mode_amps) **2)return energy
print(f"Initial transmission at center wavelength: {get_transmission_from_data(sim_data_init).item()}")transmission_init =abs(sim_data_init["mode_multiple_wl"].amps.sel(direction="+")) **2transmission_init_db =10* np.log10(transmission_init)wavelengths = td.C_0 / transmission_init.fplt.plot(wavelengths, transmission_init_db, c="black", linewidth=3)plt.xlabel("wavelength (μm)")plt.ylabel("transmission (dB)")plt.grid()plt.show()
Initial transmission at center wavelength: 0.7746249481935996
Next we can put everything together into a single objective function.
We first define the parameters to optimize and initialize our Tidy3D Adam optimizer.
# hyperparametersnum_steps =25learning_rate =0.1# initialize Adam optimizer with starting parametersparams = np.copy(params0)optimizer = adam(learning_rate=learning_rate)
And then we can run the optimization using the optimize helper, which handles the iterative parameter updates, optimizer state, and bookkeeping internally.
At each step, the gradient of the objective is used to update the parameters within the specified bounds. The callback function is used to log progress and visualize the device material density, allowing us to monitor how the design evolves over time.
Note: the following optimization loop will take about half an hour. To run fewer iterations, just change num_steps to something smaller above.
J = 9.5104e-01
grad_norm = 1.3481e-03
CPU times: user 1min, sys: 2.28 s, total: 1min 3s
Wall time: 28min 10s
Analysis
Now is the fun part! We get to take a look at our optimization results.
We first plot the objective function values over the course of optimization, which should show a steady increase. Since the curve has not yet leveled off completely, we can conclude that we could run the optimization for longer to achieve even more improvements.
Next, we can look at the performance of our optimized device, we first construct it using the final parameter values and then take a look at the design.
This optimized device seems to have some very intricate structures, which might be difficult to fabricate. This can be recifified by adding an additional loss term penalizing small structures. You can take a look at this notebook on how this may be done.
Let’s add a multi-frequency mode monitor and a field monitor to inspect the performance.
08:51:11 UTC Estimated FlexCredit cost: 0.298. 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.
08:51:12 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.
08:51:21 UTC status = preprocess
08:51:26 UTC starting up solver
running solver
08:51:35 UTC early shutoff detected at 5%, exiting.
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.