Adjoint methods are a core tool for inverse design, where the goal is to efficiently compute gradients of an objective with respect to many design parameters. In the standard adjoint workflow, Tidy3D first runs the forward simulation and then constructs the required adjoint simulations on the fly from the recorded monitor data. At that point, it determines which adjoint simulations are needed to obtain the requested gradients and runs them afterward.
For some supported monitor outputs, however, the required adjoint simulations are known in advance and do not depend on the forward result. In those cases, Tidy3D can prepare one or more adjoint simulations, launch them in parallel with the forward simulation, and combine them with the forward data afterward. This is the idea behind parallel adjoint.
To use parallel adjoint efficiently, the simulation has to be set up such that it is already clear beforehand which adjoint simulations will be needed. In practice, this means using only supported and gradient-relevant monitor outputs, and restricting those outputs to the data that actually enters the objective.
When the supported adjoint work dominates the total runtime, this can lead to a speedup approaching 2x. In this notebook, we use a simple 2D waveguide bend as a toy example and compare the runtime of a single gradient evaluation with parallel adjoint switched off and on.
import timeimport autograd as agimport autograd.numpy as anpimport matplotlib.pylab as pltimport numpy as npimport tidy3d as tdimport tidy3d.web as webfrom tidy3d.config import config# make sure that runtime measures are not impacted by cachingtd.config.web.enable_caching =Falsetd.config.local_cache.enabled =False
Setup
As a simple toy example, we use a 90 degree SiN waveguide bend and measure the gradient of the transmitted mode power with respect to a single geometric parameter. The parameter controls the waveguide width near the middle of the bend and tapers linearly back to the nominal width at the input and output.
The point here is not this particular parameterization. It is just a compact example that makes the parallel adjoint workflow easy to see.
We use a clean 2D approximation of the bend. The simulation is invariant in z, so the computational domain is collapsed with size=(Lx, Ly, 0), PML is applied only in x and y, and we use a uniform cladding background for the 2D model.
Here we define how our waveguide bend is built dependent on a single parameter by defining vertex positions. Note that this parametrization is just for demonstration purposes.
We keep the ends of the bend fixed and vary only the width towards the center. The bend starts with the nominal width at both ends, reaches a parameter-defined width at 45 degrees, and changes linearly along the arc.
Here, we add short straight input and output waveguide sections around the bend and then define the source, monitor, and full 2D simulation. The straight sections serve as clean ports: the ModeSource launches a guided mode from the input arm, and the ModeMonitor measures the outgoing modal amplitude on the output arm after the bend. The source and monitor planes are made slightly wider than the nominal waveguide using design_buffer so the modal fields are captured comfortably. Finally, make_sim() assembles the full simulation with the bend geometry, the two straight access waveguides, a uniform substrate-index background for the 2D model, automatic meshing, and PML boundaries in x and y.
Let’s visualize our experimental setup to review the correct arrangement of our components.
# the bend overlaps slightly with the straight waveguide sections by construction. Ignore this warning.td.config.logging.level ="ERROR"sim = make_sim(middle_width0)fig, ax = plt.subplots(figsize=(6, 5), tight_layout=True)sim.plot(z=0.0, ax=ax)ax.set_title("2D bend simulation")plt.show()
Select the Mode of Interest
Next, we solve for a few modes on the input waveguide cross section and assume that we are interested in the mode that appears as mode_index=2. We then tighten the source and monitor setup around this physical mode before the differentiable simulation by using to_source on the ModeSolver result and by defining ModeSpec(num_modes=1, target_neff=target_neff).
This is the key step that makes the example compatible with parallel adjoint: the target mode is fixed before the forward run, so the required adjoint simulation is already known. After tightening the mode specification, the same physical mode is tracked as mode_index=0 in the differentiable simulation.
This matters because parallel adjoint prepares adjoint simulations from the monitor outputs that may contribute to the objective. If we kept ModeSpec(num_modes=3) and only selected one mode afterward inside the objective, Tidy3D would still need to prepare adjoint simulations for all three monitored mode indices. By shrinking the ModeSpec to the one mode we actually use, the ModeMonitor contributes only one adjoint simulation in this example.
from tidy3d.plugins.mode import ModeSolverms = ModeSolver(simulation=sim, plane=mode_src, mode_spec=mode_spec, freqs=[freq0])mode_data = ms.solve()print("Effective index of computed modes:", np.array(mode_data.n_eff))
Effective index of computed modes: [[1.95681671 1.9488161 1.82578349]]
target_mode_index =2target_neff =float(np.real(np.array(mode_data.n_eff).squeeze()[target_mode_index]))# Restrict the differentiable simulation to the one physical mode we want.# After tightening the ModeSpec, this target mode is tracked as mode_index = 0.mode_src = ms.to_source( mode_index=target_mode_index, source_time=mode_src.source_time, direction=mode_src.direction,)mode_spec = td.ModeSpec(num_modes=1, target_neff=target_neff)mode_mnt = mode_mnt.updated_copy(mode_spec=mode_spec)mode_index =0# at the output plane, the guided wave travels in the negative monitor directionoutput_direction ="-"print(f"Targeting the mode that appeared as mode_index={target_mode_index} in the initial solve.")print(f"Using ModeSpec(num_modes=1, target_neff={target_neff:.4f}) for the differentiable run.")
Targeting the mode that appeared as mode_index=2 in the initial solve.
Using ModeSpec(num_modes=1, target_neff=1.8258) for the differentiable run.
Objective Function
Our figure of merit is the transmission of the selected output mode at a single target frequency.
Next, we define a small helper that toggles the relevant adjoint settings and measures the runtime of one gradient evaluation. Note that we can use the with config as cfg: context to use temporary changes which are reverted after leaving the with block.
def timed_value_and_grad(middle_width, parallel_run):with config as cfg: # optional context manager to ensure temporary changes cfg.adjoint.local_gradient = (True# note that parallel adjoint is currently only supported with local gradients ) cfg.adjoint.parallel_run = parallel_run t0 = time.perf_counter() value, grad = value_and_grad(middle_width) elapsed = time.perf_counter() - t0return {"value": float(value),"grad": float(grad),"elapsed": elapsed, }
Compare Sequential and Parallel Adjoint
We now evaluate the same gradient twice: once with parallel adjoint disabled, and once with it enabled.
In a successful run, the objective value and gradient should agree between the two evaluations (plus some numerical noise), while the parallel run finishes sooner.
The achievable speedup is not determined only by the pure simulation time. It also depends on how much of the total wall-clock time is spent in work that cannot be overlapped: constructing the simulations, uploading them, monitoring them, downloading the results, and the local postprocessing that combines forward and adjoint data. If these fixed costs are large compared to the actual solver runtime, the observed speedup will be smaller. If the supported adjoint work dominates the runtime, the speedup can approach 2x.
Rules and Guardrails
Parallel adjoint is controlled through the standard adjoint configuration. The direction policy determines how many mode directions are prepared for a ModeMonitor, and max_num_adjoint_per_fwd sets the per-run cap for parallel adjoint work.
Supported monitor outputs:
ModeMonitor amplitudes.
DiffractionMonitor amplitudes.
point FieldMonitor probes with size=(0, 0, 0) and colocate=True.
If any unsupported monitor is present in the differentiable simulation, Tidy3D falls back to the sequential adjoint pipeline for that run. Unsupported monitors include:
planar or volumetric FieldMonitor outputs.
All other monitors which are not listed above.
Direction policy for ModeMonitor outputs:
"assume_outgoing" is the default. Tidy3D infers the outgoing mode direction from the monitor position relative to the simulation center and keeps only the mode that points away from the center toward the outer simulation bounds. This avoids running adjoint simulations for the other direction.
"run_both_directions" prepares both "+" and "-" directions for each monitored mode. Use this if you are not sure in which direction the mode propagates across the monitor. Tidy3D will then run adjoint simulations for both directions, which increases the mode-monitor part of the parallel adjoint work.
How many parallel adjoint simulations can be produced?
ModeMonitor: one basis per (freq, mode_index, direction). With "assume_outgoing", this is num_freqs * num_modes. With "run_both_directions", this becomes 2 * num_freqs * num_modes.
DiffractionMonitor: one basis per (freq, order_x, order_y, polarization) for propagating orders only. In practice this is typically 2 * num_freqs * num_propagating_orders, because the polarizations are s and p.
point FieldMonitor: one basis per (freq, field_component), so num_freqs * num_components for the selected components among Ex, Ey, Ez, Hx, Hy, and Hz.
Tidy3D then groups compatible bases by port, so the number of launched adjoint simulations can be smaller than the basis count.
If the grouped count exceeds config.adjoint.max_adjoint_per_fwd, parallel adjoint is disabled and Tidy3D falls back to the sequential path.
Practical guidance:
Include only monitors in the differentiable simulation that are actually used for the gradient calculation.
Restrict each supported monitor to the essential data that enters the objective, such as the mode of interest, the relevant frequency, or the needed field components.
Place ModeMonitor outputs such that the mode of interest points away from the simulation center toward the outer boundary. This makes efficient use of the default "assume_outgoing" policy.
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.