Spiral Waveguide

Simulating large photonic structures such as spirals using full-field electromagnetic methods like FDTD can be computationally expensive and time-consuming. This notebook demonstrates how we can efficiently simulate such devices using a compact circuit model, which leverages pre-characterized components—such as bends and straights—to drastically reduce simulation cost without compromising accuracy.
We begin by defining a custom silicon nitride (SiN) technology that includes both optical and electrical material properties, as well as port and layer specifications. We then construct a rectangular spiral waveguide with a total length of 5 mm and use a circuit-level simulation approach to compute its scattering matrix. The circuit model automatically performs one-time simulations for each unique waveguide element (e.g., a representative bend or straight) and assembles the global response based on their interconnections.
To validate the accuracy of the circuit model, we also define a full Tidy3D FDTD model for the spiral. While running this simulation is expensive, we provide a precomputed S-matrix file that users can download and use directly. This allows for a side-by-side comparison of the circuit and full-field results, highlighting the efficiency and effectiveness of the circuit-based approach for large passive photonic structures.
import matplotlib.pyplot as pltimport numpy as npimport photonforge as pfimport tidy3d as tdfrom photonforge.live_viewer import LiveViewer
viewer = LiveViewer()LiveViewer started at http://127.0.0.1:33423. Call its 'stop()' method to free the port.Custom Silicon Nitride Technology
Section titled “Custom Silicon Nitride Technology”We begin by defining a custom silicon nitride (SiN) technology.
_Medium = td.components.medium.MediumType
def sin_technology( sin_thickness=0.4, sio2: dict[str, _Medium] = { "optical": td.material_library["SiO2"]["Palik_Lossless"], "electrical": td.Medium(permittivity=4.2, name="SiO2"), }, sin: dict[str, _Medium] = { "optical": td.material_library["Si3N4"]["Luke2015PMLStable"], "electrical": td.Medium(permittivity=7.5, name="Si3N4"), },): """ Creates a custom silicon nitride (SiN) technology for photonic circuit simulation.
Parameters: sin_thickness (float): Thickness of the SiN core in microns. sio2 (dict[str, Medium]): Dictionary with 'optical' and 'electrical' properties for the SiO2 cladding. sin (dict[str, Medium]): Dictionary with 'optical' and 'electrical' properties for the SiN core.
Returns: pf.Technology: A fully defined technology object for layout and simulation. """ # Define the layer for the SiN waveguide layers = { "WG": pf.LayerSpec((0, 0), description="SiN", color="#6db5dd18", pattern="/") }
# Define a mask at the base level for the SiN layer sin_mask = pf.MaskSpec((0, 0))
# Extrude the mask vertically to form the waveguide core extrusion_specs = [ pf.ExtrusionSpec( sin_mask, limits=(0, sin_thickness), medium=sin, sidewall_angle=0 ) ]
# Define ports for a 3500 nm wide strip waveguide ports = { "Strip_3500": pf.PortSpec( description="Strip waveguide, TE, 3500 nm wide", width=10, limits=(-2.5, sin_thickness + 2.5), num_modes=1, target_neff=2, path_profiles=[(3.5, 0, (0, 0))], ), }
# Assemble and return the full technology definition tech = pf.Technology( name="Custom SiN", version="1.0", layers=layers, extrusion_specs=extrusion_specs, ports=ports, background_medium=sio2, ) return tech14:28:59 -03 WARNING: The material-library variant 'Palik_Lossless' is deprecated and maps to 'Palik_LowLoss' because it contains a tiny fitted loss despite its name. Use 'Palik_NoLoss' where available for a zero-loss Palik model.
Next, we initialize the custom SiN technology and set it as the default technology. To reduce simulation cost, especially for large structures like spirals, we also set a relatively coarse mesh refinement value.
# Instantiate the custom SiN technologytech = sin_technology()
# Set it as the default technology for PhotonForgepf.config.default_technology = tech
# Define default mesh refinementpf.config.default_mesh_refinement = 14
# Define a range of wavelengthswavelengths = np.linspace(1.5, 1.6, 51)freqs = pf.C_0 / wavelengthsInspecting Port Modes
Section titled “Inspecting Port Modes”To improve the accuracy of the circuit model, we include the second-order mode by setting num_modes = 2. This is particularly important for large or tightly bent structures where higher-order modes may contribute significantly. We then visualize the mode to verify that the field is well confined and the mode shape and effective index are physically reasonable.
# Access the port specification for the 3500 nm wide stripport_spec = tech.ports["Strip_3500"]
# Interaction with second mode should be included in circuit analysisport_spec.num_modes = 2
# Solve for the port modes at a representative frequency (λ = 1.55 µm)mode_solver = pf.port_modes( port_spec, frequencies=[pf.C_0 / 1.55], mesh_refinement=40,)
# Plot the electric field magnitude (in dB) (change the mode_index to inspect different modes)_, ax = plt.subplots(1, 1, figsize=(10, 3.5), tight_layout=True)_ = mode_solver.plot_field("E", "abs^2", "dB", mode_index=0, f=pf.C_0 / 1.55, ax=ax)
# Output mode data as a DataFramemode_solver.data.to_dataframe()Uploading task 'Mode-ModeSolver…'Starting task 'Mode-ModeSolver': https://tidy3d.simulation.cloud/workbench?taskId=mo-241f2773-33c0-4afc-872b-1bbccb98289aDownloading data from 'Mode-ModeSolver'…| wavelength | n eff | k eff | loss (dB/cm) | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | ||
|---|---|---|---|---|---|---|---|---|---|
| f | mode_index | ||||||||
| 1.934145e+14 | 0 | 1.55 | 1.733006 | 0.0 | 0.0 | 0.999870 | 0.987044 | 0.880759 | 1.706345 |
| 1 | 1.55 | 1.696169 | 0.0 | 0.0 | 0.999392 | 0.949150 | 0.885144 | 1.848523 |

Spiral Simulation
Section titled “Spiral Simulation”Creating a Spiral
Section titled “Creating a Spiral”We now create a rectangular spiral waveguide with a total length of 5000 µm using the parametric.rectangular_spiral function. The spiral is built from straight and bent waveguide segments, with 80 µm bend radius and 10.5 µm separation between waveguide centers. We also specify Tidy3D as the active bend model with an Euler bend profile. Finally, we visualize the spiral layout to verify its structure.
pf.parametric.rectangular_spiral??# Define total spiral length in micronsspiral_length = 5000
# Create the rectangular spiral geometry using the specified port and bend settingsspiral = pf.parametric.rectangular_spiral( port_spec=port_spec, radius=80, full_length=spiral_length, separation=10.5, align_ports="x", bend_kwargs={ "model": pf.Tidy3DModel(port_symmetries=[("P1", "P0")]), "euler_fraction": 0.5, },)
# Visualize the spiral layoutviewer(spiral)Circuit Model Simulation
Section titled “Circuit Model Simulation”Instead of running a full electromagnetic simulation, we activate the compact circuit model for the spiral. This model automatically builds Tidy3D simulations for the bends and mode solver simulations for the straight segments to efficiently compute the individual scattering matrices over the defined frequency range. These are then combined to generate the final S-matrix for the entire structure.
Although the spiral contains many bends and straight segments, only one simulation per unique component type (e.g., a single representative bend or straight) is performed. This significantly reduces the overall simulation cost while maintaining accuracy in the circuit-level response. We simulate the response by exciting the input port P0.
# Activate the compact circuit model for the spiralspiral.activate_model("Circuit")
# Compute the S-matrix over the given frequency range using circuit simulations_matrix_circuit = spiral.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})Uploading task 'P0@0…'Uploading task 'P0@1…'Uploading task 'Mode-StripwaveguideTE3500nmwide…'Uploading task 'Mode-StripwaveguideTE3500nmwide…'Uploading task 'Mode-StripwaveguideTE3500nmwide…'Uploading task 'P0@0…'Uploading task 'P0@1…'Uploading task 'Mode-StripwaveguideTE3500nmwide…'Uploading task 'Mode-StripwaveguideTE3500nmwide…'Starting task 'P0@1': https://tidy3d.simulation.cloud/workbench?taskId=fdve-f107ac36-f81d-46a9-a1f1-5617e25745a8Starting task 'Mode-StripwaveguideTE3500nmwide': https://tidy3d.simulation.cloud/workbench?taskId=mo-a6e07cd3-541c-4d59-b479-d880cb8346f2Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-cdaab5b5-738b-485c-8dd8-85cd031efe59Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-a42037d9-d10d-4afb-aed2-d9dd1a5939fdStarting task 'Mode-StripwaveguideTE3500nmwide': https://tidy3d.simulation.cloud/workbench?taskId=mo-11aa8331-fd65-45fc-9839-af55a766dda1Starting task 'Mode-StripwaveguideTE3500nmwide': https://tidy3d.simulation.cloud/workbench?taskId=mo-57c40c79-413d-44cd-9a65-4edd54a13f9fDownloading data from 'P0@1'…Downloading data from 'Mode-StripwaveguideTE3500nmwide'…Downloading data from 'P0@0'…Downloading data from 'P0@0'…Starting task 'P0@1': https://tidy3d.simulation.cloud/workbench?taskId=fdve-ff658060-fbb6-46cd-92ff-144841d3e93eDownloading data from 'Mode-StripwaveguideTE3500nmwide'…Starting task 'Mode-StripwaveguideTE3500nmwide': https://tidy3d.simulation.cloud/workbench?taskId=mo-f52abe51-ed39-47ec-932d-6772584ad32eStarting task 'Mode-StripwaveguideTE3500nmwide': https://tidy3d.simulation.cloud/workbench?taskId=mo-a7c78db6-a492-48f4-8adf-a71a4b5dd229Downloading data from 'Mode-StripwaveguideTE3500nmwide'…Downloading data from 'P0@1'…Downloading data from 'Mode-StripwaveguideTE3500nmwide'…Downloading data from 'Mode-StripwaveguideTE3500nmwide'…Full Tidy3D Model
Section titled “Full Tidy3D Model”To benchmark the circuit model, we also define a full FDTD simulation using the Tidy3D model. Since multi-mode analysis is not necessary for the FDTD simulation, we reduce the number of modes at the ports to 1.
We expand the simulation bounds to ensure the spiral is not placed too close to the absorbing boundaries, which could affect accuracy. The simulation runtime is also increased to allow the wave sufficient time to propagate through the entire spiral structure.
It’s important to note that this full-field simulation is very computationally expensive, especially for large structures like spirals, in contrast to the much more efficient circuit model. An estimate of the simulation cost is provided to quantify this overhead.
# Limit to single-mode analysis for FDTD simulation to reduce costspiral.ports["P0"].spec.num_modes = 1spiral.ports["P1"].spec.num_modes = 1
# Get the physical bounds of the spiral structuresim_bounds = spiral.bounds()
# Create a full Tidy3D model with expanded bounds and extended simulation timetidy3dModel = pf.Tidy3DModel( bounds=( (sim_bounds[0][0] - 10, sim_bounds[0][1] - 10, None), (sim_bounds[1][0] + 10, sim_bounds[1][1] + 10, None), ), run_time=3 * spiral_length / pf.C_0 * mode_solver.data.n_eff[0][0].values,)
# Add the Tidy3D model to the spiralspiral.add_model(tidy3dModel, "Tidy3DModel")
# Estimate the cost of running the full FDTD simulationestimated_cost = tidy3dModel.estimate_cost(spiral, freqs)Uploading task 'P0@0…'Uploading task 'P1@0…'Estimated cost for task 'P1@0: 2180.799823730421'Estimated cost for task 'P0@0: 2180.799896530421'(Optional) Running Full FDTD Simulation and Saving Results
The following lines are commented out to avoid triggering a high-cost simulation by default. They compute the full S-matrix of the spiral using the Tidy3D model and save the results to a Touchstone file. Users can uncomment and run these lines if they wish to run full FDTD simulation model.
# s_matrix_tidy3d = spiral.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})# _ = s_matrix_tidy3d.write_snp("s_matrix_tidy3D_spiral.s2p")Loading Precomputed Tidy3D Results
Section titled “Loading Precomputed Tidy3D Results”To avoid the high cost of running a full Tidy3D simulation, you can download a ZIP archive of precomputed Touchstone files from the following link: s_matrix_data.
Each file corresponds to a simulation with a different mesh refinement setting (grid specification). We load these S-matrices from file, wrap them in DataModel objects, and attach them to the spiral component for analysis.
Finally, we compare the transmission spectra obtained from the circuit model and the full Tidy3D simulations. All results are plotted in dB scale.
Due to the extreme physical size of the spiral structure, achieving high accuracy with full FDTD simulation is not practically feasible—especially with limited computational resources. However, the plot below demonstrates that as we increase the mesh refinement (grid 10 → 12 → 14), the Tidy3D results progressively converge toward the circuit model prediction. This highlights the efficiency and reliability of the compact circuit modeling approach for large photonic components.
# Define the file names and grid labels for each Tidy3D resultsnp_files = { "Grid 10": "misc/s_matrix_tidy3D_spiral_grid10.s2p", "Grid 12": "misc/s_matrix_tidy3D_spiral_grid12.s2p", "Grid 14": "misc/s_matrix_tidy3D_spiral_grid14.s2p",}
# Plot Circuit Model resultplt.plot( wavelengths, 10 * np.log10(np.abs(s_matrix_circuit[("P0@0", "P1@0")]) ** 2), label="Circuit Model", linewidth=2,)
# Plot each Tidy3D-based resultfor label, file in snp_files.items(): # Load the S-matrix from the Touchstone file s_matrix = pf.SMatrix.load_snp(file)
# Wrap in a DataModel data_model = pf.DataModel(s_matrix)
# Add and activate the model on the spiral component spiral.add_model(data_model, label) spiral.activate_model(label)
# Get S-matrix at the given frequencies s_matrix_data = spiral.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})
# Plot transmission from P0 to P1 plt.plot( wavelengths, 10 * np.log10(np.abs(s_matrix_data[("P0@0", "P1@0")]) ** 2), label=f"Tidy3D {label}", linestyle="--", )
# Finalize the plotplt.xlabel("Wavelength (µm)")plt.ylabel("Transmission")plt.ylim(-30, 1)plt.legend()plt.grid(True)plt.show()