Note: the cost of running the entire notebook is larger than 10 FlexCredits.
Bayesian optimization is a popular technique for searching and optimizing a design space. It works by building a probabilistic model, such as a Gaussian process, to predict the outcome of a more complex objective function. Unlike other optimizers like genetic algorithms or particle swarm optimization, Bayesian optimization uses an acquisition function to select new potential solutions, focusing on areas of the design space that the Gaussian process predicts will have high objective values and high uncertainty. Over the course of the optimization, the model’s uncertainty reduces, improving its accuracy as a surrogate for the true objective function. This makes Bayesian optimization a direct approach to optimization, particularly well-suited to problems with small (< 30 dimensions) and expensive-to-evaluate objective functions.
However, as Bayesian optimization is an iterative process with many individual components, it can be challenging to design and parallelize, leading to long run times and high computational costs. Fortunately, the Design plugin in Tidy3D includes the Bayesian optimization method MethodBayOpt which eliminates much of this complexity. Users can quickly and easily run a Bayesian optimization for complex FDTD simulations and analyze the results. Further details on Bayesian optimization and the methods used in Tidy3D are available in the open-source Python library bayesian_optimization.
This notebook details how to perform a Bayesian optimization with the Tidy3D Design plugin for the development of a Y-junction. It is based on the work of Zhengqi Gao, Zhengxing Zhang, and Duane S. Boning, "Automatic Synthesis of Broadband Silicon Photonic Devices via Bayesian Optimization" Journal of Lightwave Technology 40, 7879-7892 (2022)DOI:10.1109/JLT.2022.3207052. A detailed description of how to build a Y-junction can be found in the Waveguide Y junction.
If you are curious about other Design plugin features, see these notebooks:
# The Bayesian optimizer uses the bayesian-optimization external package version 1.5.1.# Uncomment the following line to install the package# pip install bayesian-optimization==1.5.1import gdstkimport matplotlib.pyplot as pltimport numpy as npimport tidy3d as tdimport tidy3d.plugins.design as tddimport tidy3d.web as webfrom scipy.interpolate import make_interp_spline
Simulation Setup
The simulation is defined across the 1.5 \(\mu m\) to 1.6 \(\mu m\) wavelength range with 100 sampling points. The base of the model is silicon, the top of the model is silicon dioxide; we use the material constants included in the Tidy3D Tidy3D’s material library.
lda0 =1.55# central wavelengthfreq0 = td.C_0 / lda0 # central frequencyn_wav =100# Number of wavelengths to sample in the rangeldas = np.linspace(1.5, 1.6, n_wav) # wavelength rangefreqs = td.C_0 / ldas # frequency rangefwidth =0.5* (np.max(freqs) - np.min(freqs)) # width of the source frequency rangesi = td.material_library["cSi"]["Palik_LowLoss"]sio2 = td.material_library["SiO2"]["Palik_LowLoss"]
The following parameters describe the geometry of the waveguide. The optimizable design space is the junction which is split into 13 discrete segments.
t =0.22# thickness of the silicon layernum_d =13# dimensional space of the design regionl_in =1# input waveguide lengthl_junction =2# length of the junctionl_bend =6# horizontal length of the waveguide bendh_bend =2# vertical offset of the waveguide bendl_out =1# output waveguide lengthbranch_width =0.5# width of one Y branchbranch_sep =0.2# distance between y branches at the junctioninf_eff =100# effective infinity
The most effective way to use the Design plugin is to split the workflow into “pre” and “post” functions which can surround a call to the Tidy3D cloud that carries out the computation. This takes advantage of automated simulation batching, allowing for parallelization which saves a considerable amount of time. The pre-function returns a Simulation; the post-function then analyzes the corresponding SimulationData. Together they can be considered the fitness function (or objective function / figure of merit) which the Bayesian optimization process is working to predict.
The function fn_pre needs to take the parameters that are being optimized: these are the 13 width segments of the junction. They always input as a dictionary and can be unpacked within the function (as below) or included as keyword arguments. These parameters are used to build a Y-junction Simulation object.
The function fn_post then takes the SimulationData object output by the simulation and computes the objective function. The value of objective is then fed back into the Bayesian optimization to inform the probabilistic model. In this case, we extract the power passing through the Y-junction and the power reflected back to the source. These values are evaluated in a custom loss function described by Gao et al. to determine the effectiveness of the junction design:
where the summation is performed over the simulated wavelengths, \(N_\lambda\) is the number of wavelength points, \(R\) is the reflected power and \(T\) is the transmitted power in one branch.
The aim of this function is to achieve a transmitted power of 0.5 through each branch, whilst driving the power reflected towards the source to zero. Note there is a minus sign in front of the output value as this loss function is a minimizing function, whilst all the optimizers in the Design plugin are built to maximize the objective function.
def fn_pre(**w_params: dict) -> td.Simulation:"""Create a Simulation of a Y splitter from a series of junction widths. Includes mode monitors to measure the power transmitted and reflected to source. """ w_start =0.5 w_end = branch_width *2+ branch_sep widths = [w_start] # Ensures input waveguide is included in spline for first point of junction widths.extend(list(w_params.values())) widths.append(w_end) # Ensures final point of junction smoothly converts to the branches x_junction = np.linspace( l_in, l_in + l_junction, num_d +2 ) # x coordinates of the top edge vertices y_junction = np.array(widths) # y coordinates of the top edge vertices# pass vertices through spline and increase sampling to smooth the geometry new_x_junction = np.linspace( l_in, l_in + l_junction, 100 ) # x coordinates of the top edge vertices spline = make_interp_spline(x_junction, y_junction, k=2) spline_yjunction = spline(new_x_junction)# using concatenate to include bottom edge vertices x_junction = np.concatenate((new_x_junction, np.flipud(new_x_junction))) y_junction = np.concatenate((spline_yjunction /2, -np.flipud(spline_yjunction /2)))# stacking x and y coordinates to form vertices pairs vertices = np.transpose(np.vstack((x_junction, y_junction))) junction = td.Structure( geometry=td.PolySlab(vertices=vertices, axis=2, slab_bounds=(0, t)), medium=si ) x_start = l_in + l_junction # x coordinate of the starting point of the waveguide bends x_bend = np.linspace(x_start, x_start + l_bend, 100) # x coordinates of the top edge vertices y_bend = ( (x_bend - x_start) * h_bend / l_bend- h_bend * np.sin(2* np.pi * (x_bend - x_start) / l_bend) / (np.pi *2)+ w_end /2- w_start /2 ) # y coordinates of the top edge vertices# adding the last point to include the straight waveguide at the output x_bend = np.append(x_bend, inf_eff) y_bend = np.append(y_bend, y_bend[-1])# add path to the cell cell = gdstk.Cell("bends") cell.add( gdstk.FlexPath(x_bend +1j* y_bend, branch_width, layer=1, datatype=0) ) # top waveguide bend cell.add( gdstk.FlexPath(x_bend -1j* y_bend, branch_width, layer=1, datatype=0) ) # bottom waveguide bend# define top waveguide bend structure wg_bend_1 = td.Structure( geometry=td.PolySlab.from_gds( cell, gds_layer=1, axis=2, slab_bounds=(0, t), )[0], medium=si, )# define bottom waveguide bend structure wg_bend_2 = td.Structure( geometry=td.PolySlab.from_gds( cell, gds_layer=1, axis=2, slab_bounds=(0, t), )[1], medium=si, )# straight input waveguide wg_in = td.Structure( geometry=td.Box.from_bounds(rmin=(-inf_eff, -w_start /2, 0), rmax=(l_in, w_start /2, t)), medium=si, )# the entire model is the collection of all structures defined so far model_structure = [wg_in, junction, wg_bend_1, wg_bend_2] Lx = l_in + l_junction + l_out + l_bend # simulation domain size in x direction Ly = w_end +2* h_bend +1.5* lda0 # simulation domain size in y direction Lz =10* t # simulation domain size in z direction sim_size = (Lx, Ly, Lz)# add a mode source as excitation mode_spec = td.ModeSpec(num_modes=1, target_neff=3.5) mode_source = td.ModeSource( center=(l_in /2, 0, t /2), size=(0, 4* w_start, 6* t), source_time=td.GaussianPulse(freq0=freq0, fwidth=fwidth), direction="+", mode_spec=mode_spec, mode_index=0, )# add a mode monitor to measure transmission at the output waveguide mode_monitor_11 = td.ModeMonitor( center=(l_in /3, 0, t /2), size=(0, 4* w_start, 6* t), freqs=freqs, mode_spec=mode_spec, name="mode_11", ) mode_monitor_12 = td.ModeMonitor( center=(l_in + l_junction + l_bend + l_out /2, w_end /2- w_start /2+ h_bend, t /2), size=(0, 4* w_start, 6* t), freqs=freqs, mode_spec=mode_spec, name="mode_12", )# add a field monitor to visualize field distribution at z=t/2 field_monitor = td.FieldMonitor( center=(0, 0, t /2), size=(td.inf, td.inf, 0), freqs=[freq0], name="field" ) run_time =5e-13# simulation run time# construct simulation sim = td.Simulation( center=(Lx /2, 0, 0), size=sim_size, grid_spec=td.GridSpec.auto(min_steps_per_wvl=20, wavelength=lda0), structures=model_structure, sources=[mode_source], monitors=[mode_monitor_11, mode_monitor_12, field_monitor], run_time=run_time, boundary_spec=td.BoundarySpec.all_sides(boundary=td.PML()), medium=sio2, )return simdef fn_post(sim_data: td.SimulationData) ->float:"""Calculate the loss function from the power at the mode monitors in the SimulationData."""# Calculate the power reflected back to source and transmitted to one branch power_reflected = np.squeeze( np.abs(sim_data["mode_11"].amps.sel(direction="-", mode_index=0)) **2 ) power_transmitted = np.squeeze( np.abs(sim_data["mode_12"].amps.sel(direction="+", mode_index=0)) **2 )# Loss function proposed by Gao et al. which takes advantage of branch symmetry loss_fn =1/3* n_wav * np.sum(power_reflected**2+2* (power_transmitted -0.5) **2) output =-float(loss_fn.values) # Negative value as this is a minimizing loss functionreturn output
We can quickly check that fn_pre is working correctly by passing a set of potential test_params and plotting the result. Note that the gap between the Polyslab junction and the branches is a plotting artifact and doesn’t exist in the Simulation.
Next, we setup the Bayesian optimization method. This is done with the MethodBayOpt object. We don’t have the lcb (lower confidence bound) acquisition function used in the paper available to us, but by making our loss function negative and using the ucb (upper confidence bound) acquisition we achieve the same optimizer design. The initial_iter and n_iter options control the number of initial random samples and subsequent optimization iterations respectively. We can also set the random seed to ensure reproducible results (optional).
We also create a list of ParameterFloat objects corresponding to the 13 segment widths. The span option defines the bounds of each parameter.
The method and parameters are then passed to a DesignSpace object which contains all we need to run the Bayesian optimization.
It is then very easy to the launch this optimization with design_space.run(). This launches an initial random batch of 30 simulations, as specified by initial_iter, followed by sequential computation of 70 simulations, as specified by n_iter. In the latter phase, the Bayesian optimizer chooses potential candidates based on simultaneously maximizing the objective value and efficiently exploring the design space. The total of 100 simulations takes around 90 minutes to compute. Once complete, the results are returned in a pandas dataframe for analysis.
The best result can be extracted directly from the optimizer object. Plotting this, we see what the optimizer has returned as the optimized structure for this design of Y-junction.
We can then create a plot to evaluate if the Bayesian optimization has converged on a fitness value. The first 30 iterations are from the random initialization, so the fitness values are expected to be more varied. The fitness has converged before the end of the remaining 70 iterations; an early stop criteria could have been included to finish the optimization sooner.
11:28:54 UTC Estimated FlexCredit cost: 0.116. 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.
11:28:55 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.
11:29:07 UTC starting up solver
running solver
11:29:18 UTC early shutoff detected at 76%, exiting.
And we can visualise how the power transmitted varies over the frequency range.
power_transmitted = np.squeeze( np.abs(final_sim_data["mode_12"].amps.sel(direction="+", mode_index=0)) **2)plt.plot(freqs, power_transmitted)plt.title("Power transmitted across frequency range")plt.xlabel("Frequency / Hz")plt.ylabel("Power")plt.show()
Conclusion
Through comparison of the junction geometry and the transmitted and reflected power, we can show that these results closely follow the results published by Gao et al. for the design of a Y-junction. This notebook demonstrates how to carry out Bayesian optimization with the Design plugin, and can be readily adapted to other use cases.
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.