Custom Component Library

PhotonForge components are PCells (parameterized cells). They can be real or virtual, and they can be hierarchical.
- Models: simulation, analytical equations, or measured experimental data
- Ports: optical, electrical, or virtual
- Layout import: Cadence OpenAccess database or GDS/OASIS files
Getting Started
Section titled “Getting Started”Load the technology.
# Load the technology stack and helper importsfrom runpy import run_path
# Loading_Technology.qmd defines what this notebook builds on. Its shared cells are# generated into the module below; see docs/scripts/generate_notebook_modules.py.globals().update( {k: v for k, v in run_path("misc/loading_technology.py").items() if not k.startswith("_")})
# Reduce Tidy3D logging noise in the notebooktd.config.logging.level = "ERROR"We import a few utilities, start the interactive viewer, and set technology-specific defaults (optical port spec, default bend radius, and Tidy3D mesh refinement).
import warnings
from photonforge.live_viewer import LiveViewerfrom photonforge.stencil import as_component
# Suppress warnings about unmatched terminals and portswarnings.filterwarnings( "ignore", message="Terminal.*does not match any reference terminals")
warnings.filterwarnings( "ignore", message=".*not connected and will be ignored.*")viewer = LiveViewer()
port_spec = tech.ports["TE_1550_500"] # optical port definitionpf.config.default_kwargs["port_spec"] = port_specpf.config.default_kwargs["radius"] = 5 # default bend radius (µm)pf.config.default_kwargs["euler_fraction"] = 0.5pf.config.default_mesh_refinement = 12 # default mesh refinement for Tidy3D simulations
rib = tech.ports["Rib_TE_1550_500"]LiveViewer started at http://127.0.0.1:34337. Call its 'stop()' method to free the port.We set up the wavelength/frequency grid for frequency sweeps and define a carrier point (, ) used for single-frequency mode solves.
We also define a nominal propagation loss model (used later in analytical / compact models) so that device responses include realistic insertion loss.
wavelengths = np.linspace(1.543, 1.558, 201)lambda0 = 1.55
freqs = pf.C_0 / wavelengthsfreq0 = pf.C_0 / lambda0
# Waveguide propertiespropagation_loss = 3.0 / 10000.0 # propagation loss: 3 dB/cm (converted to dB/µm)We solve for the waveguide eigenmodes at the carrier wavelength to extract effective index and group index values used later for analytical models.
The next cell runs the mode solver for the default strip waveguide, and the following cell repeats the same workflow for the rib waveguide.
# Solve the strip waveguide mode at the carrier frequencymode_solver = pf.port_modes(port_spec, [freq0], mesh_refinement=50, group_index=True)
# Plot the mode electric-field profilemode_solver.plot_field("E", mode_index=0, f=freq0)
# Extract the fundamental-mode effective and group indicesn_eff = mode_solver.data.n_eff.isel(mode_index=0).item()n_group = mode_solver.data.n_group.isel(mode_index=0).item()
mode_solver.data.to_dataframe()Uploading task 'Mode-ModeSolver…'Starting task 'Mode-ModeSolver': https://tidy3d.simulation.cloud/workbench?taskId=mo-9aa492f6-5c1d-4747-abd0-2918b66e7b00Downloading data from 'Mode-ModeSolver'…| wavelength | n eff | k eff | loss (dB/cm) | TE (Ey) fraction | wg TE fraction | wg TM fraction | mode area | group index | dispersion (ps/(nm km)) | ||
|---|---|---|---|---|---|---|---|---|---|---|---|
| f | mode_index | ||||||||||
| 1.934145e+14 | 0 | 1.55 | 2.443192 | 0.0 | 0.0 | 0.983501 | 0.764069 | 0.81791 | 0.190715 | 4.187462 | 515.572826 |

# Solve the rib waveguide mode at the carrier frequencymode_solver_rib = pf.port_modes(rib, [freq0], mesh_refinement=50, group_index=True)
# Plot the mode electric-field profilemode_solver_rib.plot_field("E")
# Extract the fundamental-mode group indexn_group_rib = mode_solver_rib.data.n_group.isel(mode_index=0).item()
# Extract the fundamental-mode effective indexn_eff_rib = mode_solver_rib.data.n_eff.isel(mode_index=0).item()Uploading task 'Mode-ModeSolver…'Starting task 'Mode-ModeSolver': https://tidy3d.simulation.cloud/workbench?taskId=mo-3de4306f-e76f-4523-a01b-a6c5b21cc7fbDownloading data from 'Mode-ModeSolver'…
Edge Coupler
Section titled “Edge Coupler”Edge couplers provide efficient fiber-to-chip coupling by expanding the guided mode near the facet to better match the fiber mode. In this notebook we use an angled inverse-taper edge coupler geometry that is parameterized for reuse and quick sweeps.
Design Tip: High coupling efficiency requires matching the mode profiles of the chip and the fiber. We use a lensed fiber (2.5 µm spot diameter) to focus the light, while simultaneously using a narrow inverse taper tip (150 nm) to expand the waveguide mode. A long adiabatic taper is used to transition seamlessly between the standard 500 nm waveguide and the narrow tip without scattering losses.
To minimize back-reflections (return loss), the waveguide tip is angled (typically 7°–15°). A straight facet would reflect light directly back into the laser, causing instability. The angle ensures that any reflected light is steered away from the guided mode and radiated into the cladding. Return loss can be further reduced by filling the gap with an index-matching material (such as epoxy or fluid), which minimizes Fresnel reflections at the facet.
See the PhotonForge edge coupler example for more details.
@pf.parametric_component(name_prefix="Angled Fiber")def angled_edge_coupler( *, width_tip=0.1, # Width of the taper tip (µm) length_taper=10, # Length of the adiabatic taper (µm) angle_taper=7, # Angle of the waveguide facet (degrees) waist_radius=2.0, # Mode field radius of the fiber (µm) fiber_distance=3.0, # Distance from facet to fiber (µm) angle_fiber=10.2, # Angle of fiber incidence (degrees) trench_layer="Deep Trench", # Layer for the etch opening): """ Creates an angled inverse taper edge coupler for low-reflection fiber coupling.
Parameters: width_tip (float): Width of the waveguide tip (µm). length_taper (float): Length of the adiabatic taper (µm). angle_taper (float): Angle of the waveguide facet (degrees). waist_radius (float): Mode field radius of the coupling fiber (µm). fiber_distance (float): Distance between the fiber and the facet (µm). angle_fiber (float): Angle of the fiber incidence (degrees). trench_layer (str): Layer name for the deep trench opening.
Returns: Component: The edge coupler component with optical ports. """ # Get waveguide width and silicon thickness from the technology port_spec = tech.ports["TE_1550_500"] core_width, _ = port_spec.path_profile_for("Si") core_thickness = tech.parametric_kwargs["si_thickness"]
# Unit vectors for the taper direction and fiber axis (degrees -> radians) v_taper = np.array( (np.cos(angle_taper / 180 * np.pi), np.sin(angle_taper / 180 * np.pi)) ) v_fiber = np.array( (np.cos(angle_fiber / 180 * np.pi), np.sin(angle_fiber / 180 * np.pi)) )
# Build the silicon inverse taper endpoint = pf.snap_to_grid(length_taper * v_taper) taper = ( pf.Path(-waist_radius * v_taper, width_tip) .segment((0, 0)) .segment(endpoint, core_width) )
# Taper endpoint after a small bend to guarantee that the waveguide port # grid-aligned if angle_taper > 0: radius = 5 endpoint = pf.snap_to_grid( endpoint + radius * np.array((abs(v_taper[1]), 1 - v_taper[0])) ) base = 90 if angle_taper > 0 else -90 taper.arc(90 + angle_taper, 90, radius, endpoint=endpoint)
component = pf.Component(technology=siepic.ebeam())
# Define the fiber port position from distance and fiber angle fiber_center = -fiber_distance * v_fiber port_fiber = pf.GaussianPort( center=(fiber_center[0], fiber_center[1], core_thickness / 2), input_vector=(v_fiber[0], v_fiber[1], 0), waist_radius=waist_radius, polarization_angle=0, # TE polarization )
# Define the on-chip port at the end of the taper port_waveguide = pf.Port(endpoint, 180, port_spec)
component.add_port([port_fiber, port_waveguide])
# Add taper and trench opening near the facet # The size of Trench defines the boundary of the simulation so it is important to be sufficiently large component.add( "Si", taper, trench_layer, pf.Rectangle( (-fiber_distance - waist_radius, -5 * waist_radius), ( 0, max( 5 * waist_radius, port_waveguide.center[1] + port_spec.width / (2 * v_taper[0]), ), ), ), )
model = pf.Tidy3DModel() component.add_model(model, "Tidy3D") return componentangle_fiber = 15 # degrees# Estimate taper angle using Snell's law (n ≈ 1.45)angle_taper = np.arcsin(np.sin(angle_fiber / 180 * np.pi) / 1.45) / np.pi * 180print(f"Taper angle: {angle_taper}°")
edge_coupler = angled_edge_coupler( width_tip=0.15, length_taper=25, angle_taper=angle_taper, waist_radius=2.5 / 2, fiber_distance=3, angle_fiber=angle_fiber,)
# Visualize the geometryviewer(edge_coupler)Taper angle: 10.282162084410936°# Simulate and plot the edge-coupler S-matrixs_matrix_ec = edge_coupler.s_matrix(freqs)_ = pf.plot_s_matrix(s_matrix_ec, y="dB", input_ports=["P0"])Uploading task 'P0@0…'Uploading task 'P1@0…'Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-4e361375-de71-4eed-80b3-05466ec46792Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-fd4b11e4-4232-45ae-9c99-944fac6ad19aDownloading data from 'P0@0'…Downloading data from 'P1@0'…
Monitor Tap
Section titled “Monitor Tap”This section defines a weak directional coupler used as a monitor tap (99:1) to sample a small fraction of the optical power for calibration and control.
Design Tip: To achieve a weak coupling ratio (1%), we must limit the power transfer between waveguides. This is typically done by increasing the gap or decreasing the coupling length.
Because the coupling coefficient is sensitive to geometry, the precise parameters are best found via a parameter sweep. A common approach is to fix the gap (e.g., a fabrication-safe value like 300 nm) and sweep the coupling length to dial in the exact 1% split.
@pf.parametric_component(name_prefix="Monitor Tap")def monitor_tap( *, coupling_gap=0.3, # gap between waveguides (µm) coupling_length=2.4, # coupling region length (µm) coupling_offset=5, # S-bend offset for coupler (µm) s_bend_length=20, # drop-arm S-bend length (µm) s_bend_offset=10, # drop-arm offset (µm)): """ 99:1 S-bend directional coupler.
99% transmission on the through port (P0 -> P1). 1% coupling to the tap port (P0 -> P2, routed via S-bend). """ wg_width, _ = port_spec.path_profile_for("Si")
# Create the directional coupler tidy3d_model = pf.Tidy3DModel( verbose=True, port_symmetries=[ ("P1", "P0", "P3", "P2"), ("P2", "P3", "P0", "P1"), ("P3", "P2", "P1", "P0"), ], ) coupler = pf.parametric.s_bend_coupler( coupling_distance=wg_width + coupling_gap, coupling_length=coupling_length, s_bend_offset=coupling_offset, model=tidy3d_model, )
# Create an S-bend to route the monitor port s_bend = pf.parametric.s_bend( length=s_bend_length, offset=s_bend_offset, model=pf.WaveguideModel(), )
# Assemble the component via a netlist netlist = { "instances": {"COUPLER": coupler, "S_BEND": s_bend}, "connections": [ (("S_BEND", "P0"), ("COUPLER", "P3")), ], "ports": [ ("COUPLER", "P0"), # Input ("COUPLER", "P2"), # Through (99%) ("S_BEND", "P1"), # Monitor tap (1%) ("COUPLER", "P1"), # Drop (unused) ], "models": [pf.CircuitModel()], }
return pf.component_from_netlist(netlist)
pm = monitor_tap()viewer(pm)# Simulate and plot the power monitor S-matrixs_matrix_pm = pm.s_matrix(freqs)_ = pf.plot_s_matrix(s_matrix_pm, input_ports=["P0"])14:33:30 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-5c9a99db-692 4-4ed7-801f-848c54e45412'.
Uploading task 'Mode-StripTE1550nmw500nm…'Starting task 'Mode-StripTE1550nmw500nm': https://tidy3d.simulation.cloud/workbench?taskId=mo-c74a11a8-1b44-408f-bbe9-5758d4e8b985Downloading data from 'Mode-StripTE1550nmw500nm'…
Monitor Photodiode
Section titled “Monitor Photodiode”The monitor photodiode is an integrated detector used to convert a fraction of the optical signal into an electrical current for power tracking. This component is modeled after the Keysight N1030A specifications, providing a high-bandwidth (65 GHz) response for real-time monitoring of signal integrity and link performance.
@pf.parametric_component(name_prefix="Monitor PD")def monitor_photodiode( *, width=100, # detector box size (µm) pad_width=25, # metal pad size (µm)): """ Monitor photodiode with a realistic 65 GHz detector model.
Based on Keysight N1030A specifications (65 GHz unamplified module). Includes an optical input port and electrical terminals for readout. """ c = pf.Component()
# Photodetector active area (black box) rect = pf.Rectangle(center=(0, 0), size=(width, width)) c.add("Dream Photonics Black Box-Not Fabricated", rect)
# Add a text label text_comp = as_component( layer="Text", stencil="text", text_string="Monitor PD", size=10.0, ) text_ref = pf.Reference(text_comp, origin=(-width / 4, 0)) c.add_reference(text_ref)
# Optical input port port_in = pf.Port( center=(-width / 2.0, 0), spec=port_spec, input_direction=0, ) # Electrical output port port_out = pf.Port( center=(width / 2.0, 0), spec=pf.virtual_port_spec(classification="electrical"), input_direction=0, ) c.add_port([port_in, port_out])
# Metal pads for electrical readout (GSG configuration) rect_signal = pf.Rectangle(center=(0, 0), size=(pad_width, pad_width)) rect_signal.y_min = pad_width / 2.0 rect_signal.x_max = width / 2.0
rect_ground = rect_signal.copy() rect_ground.y_max = -pad_width / 2.0
c.add("M2_router", rect_signal, rect_ground)
# Electrical terminals signal_pad = pf.Terminal("M2_router", rect_signal.copy()) ground_pad = pf.Terminal("M2_router", rect_ground.copy()) c.add_terminal([signal_pad, ground_pad])
# Use one-port termination model to enable frequency domain simulations detector_model = pf.TerminationModel()
# Photodiode time-domain model (Keysight N1030A, 65 GHz) detector_model.time_stepper = pf.PhotodiodeTimeStepper( responsivity=0.85, # A/W (InGaAs at 1550 nm) gain=50.0, # V/A (50 Ω load) saturation_current=6.8e-3, # A (8 mW max input) dark_current=10e-9, # A (10 nA typical) thermal_noise=3.3e-22, # A^2/Hz (Johnson noise) filter_frequency=65e9, # Hz (65 GHz bandwidth) roll_off=2, ) c.add_model(detector_model, "Photodiode")
return c
pd = monitor_photodiode()viewer(pd)Y-Splitter
Section titled “Y-Splitter”This section defines a splitter/combiner for distributing optical power between one input and two outputs. We use a PDK component.
Design Tip: One of the most robust approaches for low-loss splitting is an adiabatic Y-junction. By gradually expanding the input waveguide, the fundamental mode evolves smoothly into the two output arms with minimal scattering or reflection.
Fabrication Note: Because perfectly sharp tips are impossible to manufacture, a “healed” geometry (blunted tip) is used to ensure the gap size respects the foundry’s minimum feature size (e.g., 60 nm) while maintaining adiabatic behavior.
# Load and visualize an adiabatic Y-junction splitter PCellsplitter = siepic.component("ebeam_y_1550")viewer(splitter)# Simulate and plot the splitter S-matrixs_matrix_splitter = splitter.s_matrix(freqs)_ = pf.plot_s_matrix(s_matrix_splitter, input_ports=["P0"])# Add the simulation results as a DataModel to avoid future FDTD simulations for different frequencies# splitter.add_model(pf.DataModel(s_matrix_splitter))14:34:24 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-3b1440c5-2e6 9-4d29-93a1-add71dfa93c7'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-96c9bbec-a87 7-4ccf-b33e-f37939ad32a3'.

Directional Couplers for the MUX
Section titled “Directional Couplers for the MUX”We will build a 4-channel MUX using a dual-ring resonator.
Reference:
De Heyn, P., et al., “Fabrication-Tolerant Four-Channel Wavelength-Division-Multiplexing Filter Based on Collectively Tuned Si Microrings,” Journal of Lightwave Technology, 2013 31 (16), 2785–2792, doi: 10.1109/JLT.2013.2273391.
First, we define a directional coupler component. A grid_spec of 25 is used to ensure the high mesh accuracy necessary for simulating resonant structures.
@pf.parametric_component(name_prefix="Directional Coupler")def directional_coupler( *, coupling_gap=0.340, # gap between waveguides (µm) radius=5, # bend radius (µm) coupling_length=9, # coupling region length (µm) heater_width=4.0, # heater width (µm)): """ Tunable directional coupler with thermal heaters.
Used for: - Ring-to-ring coupling in add-drop filters - Bus-to-ring coupling in resonators """ wg_width, _ = port_spec.path_profile_for("Si") coupling_distance = wg_width + coupling_gap
component = pf.parametric.dual_ring_coupler( coupling_distance=coupling_distance, radius=radius, coupling_length=coupling_length, model=pf.Tidy3DModel( grid_spec=25, port_symmetries=[ ("P1", "P0", "P3", "P2"), ("P2", "P3", "P0", "P1"), ("P3", "P2", "P1", "P0"), ], ), )
left_corner = component.ports["P1"].center - ( 0, heater_width / 2 + radius + coupling_distance / 2, ) right_corner = component.ports["P3"].center + ( 0, heater_width / 2 - radius - coupling_distance / 2, ) component.add("M1_heater", pf.Rectangle(left_corner, right_corner))
return componentTwo-Ring Resonator Filter
Section titled “Two-Ring Resonator Filter”This section builds a two-ring add–drop resonator filter (2RR) as a reusable WDM building block, including heaters for thermal tuning.
Design Tip: To achieve a flat-top (maximally flat) passband in a two-ring add–drop filter (with identical rings), choose the coupling gaps such that the bus-to-ring and ring-to-ring coupling coefficients satisfy the following synthesis relation:
where:
- : bus-to-ring (external) power coupling coefficient ( in our design)
- : ring-to-ring (internal) power coupling coefficient ( in our design)
This condition ensures a maximally flat spectral response by properly balancing external and internal coupling strengths.
dc_rr = directional_coupler(coupling_gap=0.348)viewer(dc_rr)# Run a finer wavelength sweep to visualize the 2RR filter responses_matrix_rr = dc_rr.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})_ = pf.plot_s_matrix(s_matrix_rr, input_ports=["P0"])p3 = np.abs(s_matrix_rr[("P0@0", "P3@0")]) ** 2p2 = np.abs(s_matrix_rr[("P0@0", "P2@0")]) ** 2print( f"The internal coupling ratio at the center wavelength is: {p3[len(freqs)//2]/p2[len(freqs)//2]:.4f}")Uploading task 'P0@0…'Starting task 'P0@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-c8dde4c0-f2f9-4f09-a376-351cc62f5efdDownloading data from 'P0@0'…The internal coupling ratio at the center wavelength is: 0.0210
dc_br = directional_coupler(coupling_gap=0.22)viewer(dc_br)s_matrix_br = dc_br.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})_ = pf.plot_s_matrix(s_matrix_br, input_ports=["P0"])p3 = np.abs(s_matrix_br[("P0@0", "P3@0")]) ** 2p2 = np.abs(s_matrix_br[("P0@0", "P2@0")]) ** 2print( f"The external coupling ratio at the center wavelength is: {p3[len(freqs)//2]/p2[len(freqs)//2]:.3f}")14:34:29 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-9f729794-8ae 2-4a51-8c2f-26e5d6ac6b4b'.
The external coupling ratio at the center wavelength is: 0.211
@pf.parametric_component(name_prefix="2RR Filter")def filter_2rr( *, coupling_length=9.0, # coupling length (µm) radius=5.0, # ring radius (µm) rr_gap=0.348, # ring-to-ring gap (µm) br_gap=0.220, # bus-to-ring gap (µm) pad_width=10, # bond pad width (µm)): """ Two-ring resonator (2RR) add-drop filter.
Architecture: - Two rings coupled via a ring-to-ring coupler. - Each ring coupled to the bus waveguide. - Thermal tuning via heaters. """ # Create coupler components dc_2rr = directional_coupler( coupling_gap=rr_gap, radius=radius, coupling_length=coupling_length, )
dc_2br = directional_coupler( coupling_gap=br_gap, radius=radius, coupling_length=coupling_length, )
# Create helper components bend = pf.parametric.bend() # 90 deg bend (default) wg = pf.parametric.straight(length=60)
# Assemble the 2RR filter via a netlist netlist_2rr = { "instances": { "bus_coupler0": dc_2br, "bus_coupler1": dc_2br, "ring_coupler0": dc_2rr, "bend0": bend, "bend1": bend, "wg0": wg, "wg1": wg, "wg2": wg, }, "connections": [ (("ring_coupler0", "P0"), ("bus_coupler0", "P1")), (("bus_coupler1", "P3"), ("ring_coupler0", "P1")), (("bend0", "P0"), ("bus_coupler1", "P2")), (("bend1", "P1"), ("bus_coupler1", "P0")), (("wg0", "P1"), ("bus_coupler0", "P0")), (("wg1", "P1"), ("bend0", "P1")), (("wg2", "P1"), ("bus_coupler0", "P2")), ], "ports": [ ("wg0", "P0"), ("wg1", "P0"), ("wg2", "P0"), ("bend1", "P0"), ], "models": [pf.CircuitModel()], }
two_rr = pf.component_from_netlist(netlist_2rr)
# Add bond pads for heater control (heater terminals) p0_center = np.array(dc_2br.ports["P0"].center) p3_center = np.array(dc_2br.ports["P3"].center)
bp1 = pf.Rectangle(center=p0_center, size=(pad_width, 2 * pad_width)) bp1.x_max = p0_center[0] + pad_width / 2.0 - 1.0 bp1.y_max = p0_center[1] - pad_width / 2.0
rect1 = pf.Rectangle(center=p0_center, size=(pad_width / 2.0, pad_width * 4)) rect1.x_max = bp1.x_max rect1.y_min = bp1.y_max
bp2 = bp1.copy() bp2.x_min = p3_center[0] - pad_width / 2.0 + 1.0 bp2.y_min = p3_center[1] + pad_width / 2.0 + 4 * radius
rect2 = rect1.copy() rect2.x_min = bp2.x_min rect2.y_max = bp2.y_min
two_rr.add("M2_router", bp1, rect1, bp2, rect2)
signal_pad = pf.Terminal("M2_router", bp1.copy()) ground_pad = pf.Terminal("M2_router", bp2.copy()) two_rr.add_terminal([signal_pad, ground_pad])
return two_rr
two_rr = filter_2rr()viewer(two_rr)s_matrix_2rr = two_rr.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})_ = pf.plot_s_matrix(s_matrix_2rr, y="dB", input_ports=["P0"])14:34:31 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-9f729794-8ae 2-4a51-8c2f-26e5d6ac6b4b'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-81e9dd57-705f- 4fe7-8c7e-267916c7d770'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-04e86392-7676- 4da4-8faf-a9e2f899cb00'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-c8dde4c0-f2f 9-4f09-a376-351cc62f5efd'.
14:34:32 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-b1c209d6-3627- 47fe-b922-bd31f0fad990'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-ab36a9bc-4576- 4ccb-a9f2-392fbfde3007'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-6e9b88da-d14c- 42bc-b629-871e05f7da7c'.

Four-Channel WDM
Section titled “Four-Channel WDM”When cascading multiple resonator filters into a WDM, the ring free spectral range (FSR) and the desired channel spacing must be co-designed to avoid channel collisions and routing ambiguity.
Design Tip: WDM Channel Spacing & FSR
To create a 4-channel WDM, we stagger the resonance of each filter by 1/4 of the Free Spectral Range (FSR).
Theory:
-
Free Spectral Range (FSR): The frequency spacing between consecutive resonances is determined by the group index () and total ring perimeter ():
-
Channel Shift: We need to shift the resonance by , which corresponds to a phase shift of . The required change in ring perimeter () is:
Since our parameterized
coupling_lengthadds to the ring twice (once per side), the requiredadded_lengthparameter is:
L_ring = 2 * np.pi * 5 + 2 * 9 # approximate perimeter (µm)
# Compute the FSR and required resonance shiftfsr_nm = (lambda0**2) / (n_group * L_ring) * 1000added_length = pf.snap_to_grid(lambda0 / (8 * n_eff))
print(f"Est. FSR: {fsr_nm:.1f} nm")print(f"Required added_length: {added_length*1000:.1f} nm")Est. FSR: 11.6 nmRequired added_length: 79.0 nm@pf.parametric_component(name_prefix="WDM 4ch")def wdm_4channel( *, coupling_length=9.0, # base coupling length (µm) added_length=added_length, # incremental length per channel (µm) radius=5, # ring radius (µm)): """ 4-channel C-band WDM using cascaded 2RR filters.
Each 2RR uses a slightly different coupling length to tune the resonance to a different wavelength. """ # Create four filters with incremental coupling lengths two_rr_0 = filter_2rr(coupling_length=coupling_length, radius=radius) two_rr_1 = filter_2rr(coupling_length=coupling_length + added_length, radius=radius) two_rr_2 = filter_2rr( coupling_length=coupling_length + 2 * added_length, radius=radius ) two_rr_3 = filter_2rr( coupling_length=coupling_length + 3 * added_length, radius=radius )
# Output waveguide wg = pf.parametric.straight(length=60)
# Netlist for 4-channel MUX netlist = { "instances": { "2rr0": two_rr_0, "2rr1": two_rr_1, "2rr2": two_rr_2, "2rr3": two_rr_3, "wg": wg, }, "connections": [ (("2rr1", "P1"), ("2rr0", "P3")), (("2rr2", "P1"), ("2rr1", "P3")), (("2rr3", "P1"), ("2rr2", "P3")), (("wg", "P0"), ("2rr3", "P3")), ], "ports": [ ("2rr0", "P1"), # Input (P0) ("2rr0", "P0"), # Ch1 add (P1) ("2rr0", "P2"), # Ch1 drop (P2) ("2rr1", "P0"), # Ch2 add (P3) ("2rr1", "P2"), # Ch2 drop (P4) ("2rr2", "P0"), # Ch3 add (P5) ("2rr2", "P2"), # Ch3 drop (P6) ("2rr3", "P0"), # Ch4 add (P7) ("2rr3", "P2"), # Ch4 drop (P8) ("wg", "P1"), # Through output (P9) ], # Terminals will be used for electrical routing "terminals": [ ("2rr0", "T0"), ("2rr0", "T1"), ("2rr1", "T0"), ("2rr1", "T1"), ("2rr2", "T0"), ("2rr2", "T1"), ("2rr3", "T0"), ("2rr3", "T1"), ], "models": [pf.CircuitModel()], }
return pf.component_from_netlist(netlist)
wdm = wdm_4channel()viewer(wdm)s_matrix_wdm = wdm.s_matrix(freqs, model_kwargs={"inputs": ["P0"]})_ = pf.plot_s_matrix(s_matrix_wdm, y="dB", input_ports=["P0"])14:34:35 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-9f729794-8ae 2-4a51-8c2f-26e5d6ac6b4b'.
14:34:36 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-c8dde4c0-f2f 9-4f09-a376-351cc62f5efd'.
14:34:37 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-47c76bab-9d2 e-44fd-ab98-8d842c32474d'.
14:34:38 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-4193fea1-0ff a-4cb7-ada7-dc4ee3057fb0'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-e40a603f-cec 9-479c-845e-3862df12dc5a'.
14:34:39 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-ab3e6d5a-dc9 d-47e1-a23d-3b686173a10b'.
14:34:40 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-e33d7eca-a4a c-474f-9820-ca36cc658afa'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-76405fb1-dc5 1-4368-a5f7-e4496195d502'.

Thermo-Optic Phase Shifter
Section titled “Thermo-Optic Phase Shifter”@pf.parametric_component(name_prefix="Thermo-Optic Phase Shifter")def thermo_optic_phase_shifter( *, heater_length=100, # heater length (µm) heater_width=5, # heater width (µm) pad_width=25, # bond pad width (µm) heater_overlap=5, # heater overlap into pad (µm) dn_dT=1.8e-4, # Temperature sensitivity for effective index (K⁻¹) temperature=293.0, # Waveguide temperature (K)): """ Thermo-optic phase shifter using a resistive heater.
Architecture: - Straight waveguide - Resistive heater (M1_heater) above waveguide - Bond pads (M2_router) for electrical connections
Phase shift achieved through the thermo-optic effect: Δφ ≈ (2π/λ) × dn/dT × ΔT × L
For Si at 1550 nm: dn/dT ≈ 1.8×10⁻⁴ K⁻¹ (as an approximation, same value can be used for effective index) Typical efficiency: ~10-20 mW for π phase shift """ # Create a straight waveguide swg = pf.parametric.straight(length=heater_length) thermal_model = pf.AnalyticWaveguideModel( n_eff=n_eff, reference_frequency=freq0, length=heater_length, propagation_loss=propagation_loss, n_group=n_group, dn_dT=dn_dT, temperature=temperature, ) swg.add_model(thermal_model)
# Create bond pads pad1 = pf.Path((-pad_width * 1.5, 0), width=pad_width).segment( (-0.5 * pad_width, 0) ) pad2 = pad1.copy() pad2.x_min = heater_length + 0.5 * pad_width
# Add terminals signal_pad = pf.Terminal("M2_router", pad1.copy()) ground_pad = pf.Terminal("M2_router", pad2.copy()) swg.add_terminal([signal_pad, ground_pad])
# Create heater path # Path: pad1 → taper → narrow heater → taper → pad2 heater = ( pf.Path((-0.5 * pad_width - heater_overlap, 0), pad_width) .segment((-0.5 * pad_width, 0), pad_width) .segment((0, 0), heater_width) .segment((heater_length, 0), heater_width) .segment((heater_length + 0.5 * pad_width, 0), pad_width) .segment((heater_length + 0.5 * pad_width + heater_overlap, 0), pad_width) )
# Add pads and heater to component swg.add("M2_router", pad1, pad2) swg.add("M1_heater", heater)
return swg
heater = thermo_optic_phase_shifter()viewer(heater)Thermo-Optic Tuning Verification This simulation compares the phase response of the heater at two different temperatures (300 K and 330 K) to verify the tuning efficiency. By extracting the phase of the parameter, we can estimate the phase shift per degree Kelvin, ensuring the shifter provides sufficient range for thermal compensation or signal modulation.
# Compare phase response at two temperatures to estimate thermo-optic tuningheater.update(temperature=330)s_heater_hot = heater.s_matrix(freqs)heater.update(temperature=293)s_heater_cold = heater.s_matrix(freqs)plt.plot( wavelengths, np.unwrap(np.angle(s_heater_cold[("P0@0", "P1@0")])), label="Cold (293 K)",)plt.plot( wavelengths, np.unwrap(np.angle(s_heater_hot[("P0@0", "P1@0")])), label="Hot (330 K)",)plt.xlabel("Wavelength (µm)")plt.ylabel("Phase (rad)")plt.legend()plt.grid(True)plt.tight_layout()
PN Phase Shifter
Section titled “PN Phase Shifter”An MZM converts phase modulation into intensity modulation using two interferometer arms with phase shifters.
Design Tip: MZM Length vs. Extinction Ratio
Trade-off: Determining the optimal phase shifter length is a balance between signal quality and performance.
- Too Short: You won’t achieve the required phase shift for your available voltage swing (), resulting in a poor Extinction Ratio (ER) (the “eye” won’t open fully).
- Too Long: You get excellent ER, but you suffer from higher optical loss (absorption from dopants) and a larger footprint.
The Math: To find the “sweet spot” (minimum length for a target ER), we work backward from the desired contrast:
-
Target Phase Shift (): For a push-pull MZM biased at quadrature, the ER is determined by the peak phase excursion :
-
Required : We calculate how efficient the modulator needs to be to achieve that given your drive voltage (where ):
-
Required Length (): Finally, we use the modulator’s efficiency metric, to find the physical length:
def calculate_mzm_length(target_er_db, vpi_l, v_applied): """ Calculates the required length of an MZM for a given Extinction Ratio. The Bias point is assumed to be 'quadrature' (max slope) for this calculation.
Parameters: - target_er_db: Desired Extinction Ratio in dB - vpi_l: Push-Pull Drive Modulation efficiency product (V*cm) - v_applied: Peak-to-peak drive voltage (Volts)
Returns: - length: Required length in um """
# Convert ER from dB to linear power ratio (Pmax / Pmin) er_linear = 10 ** (target_er_db / 10.0)
# Determine the required modulation index (sin(phi)) sin_phi = (er_linear - 1) / (er_linear + 1)
# Calculate the phase shift (phi) in radians phi_rad = np.asin(sin_phi)
# Determine V_peak (half of v_applied) v_peak = v_applied / 2.0
# Calculate the required Half-Wave Voltage (V_pi) if phi_rad == 0: return 0.0, 0.0 # Avoid division by zero
v_pi_req = (np.pi * v_peak) / phi_rad
# Calculate required Length length = vpi_l / v_pi_req * 1e4
return length
# --- Configuration ---target_er = 3.0 # dBvpi_L_v_cm = 1.0 # Push-pull drive (V.cm)voltage_pp = 2.0 # Volts
# --- Calculation ---ps_length = calculate_mzm_length(target_er, vpi_L_v_cm, voltage_pp)Important Note: The value used above is for the push-pull system (combined efficiency). When defining the physics for a single phase-shifter arm (as we do in the component definition below), use twice this value (i.e., ).
RF Design & Impedance Matching
Section titled “RF Design & Impedance Matching”High-speed traveling-wave modulators require careful RF engineering to ensure the electrical signal efficiently drives the optical phase shifter without debilitating reflections. We define an RF CPW port spec so electro-optic components can expose consistent electrical terminals.
Design Tip: A complete physical model of a PN modulator must account for the distributed RC loading of the active PN junction, which lowers the transmission line’s impedance and slows the RF phase velocity. Because modeling this active loading is highly complex, we will start by designing a baseline, unloaded 50 Ω Coplanar Waveguide (CPW).
The primary physical knobs for tuning the characteristic impedance of a CPW are the signal trace width and the gap spacing. Two quick rules of thumb:
- Increasing the gap increases the impedance (less capacitive coupling to ground).
- Increasing the signal width decreases the impedance (more surface area, higher capacitance).
# Create a CPW port spec for RF electrodesfreqs_rf = np.linspace(2, 30, 11) * 1e9cpw_spec = pf.cpw_spec("M2_router", signal_width=35.0, gap=10.0, ground_width=75)tech.add_port("CPW", cpw_spec)Layers
| Name | Layer | Description | Color | Pattern | Device marker | Conductive | Min. width (μm) | Min. spacing (μm) |
|---|---|---|---|---|---|---|---|---|
| Si | (1, 0) | SiEPIC - Waveguide | #ff80a818 | \\ | ||||
| PinRec | (1, 10) | SiEPIC | #ff80a818 | xx | ||||
| PinRecM | (1, 11) | SiEPIC | #80000018 | + | ||||
| Si Slab | (2, 0) | Dedicated Run Layers - Device…… Layer Partial Etch | #c080ff18 | / | ||||
| Direct Metal | (5, 0) | Dedicated Run Layers | #80a8ff18 | || | ||||
| Oxide open to BOX | (6, 0) | Dedicated Run Layers | #ff000018 | - | ||||
| Text | (10, 0) | Text-Not Fabricated | #00000018 | hollow | ||||
| M1_heater | (11, 0) | TiW Heater | #0000ff18 | \\ | ||||
| M2_router | (12, 0) | TiW/Au Routing Bilayer | #ffbf0018 | // | ||||
| M_Open | (13, 0) | Bond Pad Open | #80005718 | \\ | ||||
| Si n | (20, 0) | Dedicated Run Layers | #afff8018 | - | ||||
| Si p | (21, 0) | Dedicated Run Layers | #ffd9df18 | = | ||||
| Si n+ | (22, 0) | Dedicated Run Layers | #ff800018 | x | ||||
| Si p+ | (23, 0) | Dedicated Run Layers | #ddff0018 | xx | ||||
| Si n++ | (24, 0) | Dedicated Run Layers | #00ffff18 | + | ||||
| Si p++ | (25, 0) | Dedicated Run Layers | #00800018 | ++ | ||||
| ANT Reserved | (31, 0) | SiEPIC/ANT Reserved | #9580ff18 | / | ||||
| ANT Reserved 1 | (33, 0) | ANT Reserved | #9580ff18 | / | ||||
| Via to silicon | (40, 0) | Dedicated Run Layers | #0000ff18 | . | ||||
| DevRec | (68, 0) | SiEPIC | #00800018 | . | ||||
| FbrTgt | (81, 0) | SiEPIC/Dedicated Run Layers | #80808018 | ++ | ||||
| ANT Reserved 2 | (102, 0) | ANT Reserved | #9580ff18 | / | ||||
| ANT Reserved 3 | (110, 0) | ANT Reserved | #9580ff18 | / | ||||
| Custom Dicing | (189, 0) | #00000018 | hollow | |||||
| SEM Imaging | (200, 0) | #ff000018 | x | |||||
| Deep Trench | (201, 0) | #00ff0018 | . | |||||
| Deep Trench Handling Exclusion | (202, 0) | #00760018 | : | |||||
| Thermal Isolation Trenches | (203, 0) | #00800018 | \ | |||||
| Laser Integration Shelf | (205, 0) | Dedicated Run Layers | #69ff0518 | xx | ||||
| Floor Plan-Not Fabricated | (290, 0) | #c080ff18 | hollow | |||||
Error: device layer width is…… less than design rule | (301, 0) | DRC Errors | #80005718 | = | ||||
Error: device layer spacing is…… less than design rule | (301, 1) | DRC Errors | #80005718 | - | ||||
Warning: polygons/paths on…… PinRec layer (1/10) will NOT be fabricated | (301, 2) | DRC Errors | #80005718 | || | ||||
Error: direct metal width is…… less than 5 microns | (305, 0) | DRC Errors | #80808018 | ++ | ||||
Error: direct metal spacing is…… less than 10 microns | (305, 1) | DRC Errors | #80808018 | + | ||||
Error: TiW width is less than 3…… microns | (311, 0) | DRC Errors | #ffa08018 | // | ||||
Error: TiW spacing is less than…… 3 microns | (311, 1) | DRC Errors | #ffa08018 | / | ||||
Error: Al width is less than…… design rule | (312, 0) | DRC Errors | #00ffff18 | | | ||||
Error: Al spacing is less than…… design rule | (312, 1) | DRC Errors | #00ffff18 | // | ||||
Error: Spacing between TiW and…… Al is less than 5 microns | (312, 3) | DRC Errors | #00ffff18 | \\ | ||||
Error: Oxide window width is…… less than 10 microns | (313, 0) | DRC Errors | #01ff6b18 | || | ||||
Error: Oxide window spacing is…… less than 10 microns | (313, 1) | DRC Errors | #01ff6b18 | | | ||||
Error: Oxide window is not…… placed over Al | (313, 2) | DRC Errors | #01ff6b18 | // | ||||
| Standard Design Area | (350, 0) | DRC Errors | #ddff0018 | \ | ||||
Error: Features outside design…… area. Verify design size and centering. | (350, 1) | DRC Errors | #ddff0018 | : | ||||
Error: Dicing lane width is…… less than 100 microns | (389, 0) | DRC Errors | #ff00ff18 | ++ | ||||
Error: Spacing between dicing…… lane and devices is less than 50 microns | (389, 1) | DRC Errors | #ff00ff18 | + | ||||
Error: SEM width is less than…… 500 nm | (400, 0) | DRC Errors | #ff9d9d18 | x | ||||
| Deep Trench Design Area | (401, 0) | DRC Errors | #80a8ff18 | xx | ||||
Error: Metal, SEM, or handling…… region overlap with deep trenches. Verify design centering | (401, 1) | DRC Errors | #80a8ff18 | x | ||||
Warning: Silicon features…… outside deep trench design area. Verify accuracy before submission | (401, 2) | DRC Errors | #80a8ff18 | = | ||||
Error: Spacing between metal…… and deep trench is less than 30 microns | (401, 3) | DRC Errors | #80a8ff18 | - | ||||
Error: Deep trench width is…… less than 260 microns | (401, 4) | DRC Errors | #80a8ff18 | || | ||||
Error: Deep trench handling…… area missing. Please add handling area of size shown by polygons | (402, 0) | DRC Errors | #ff000018 | + | ||||
Error: Features inside deep…… trench handling area | (402, 1) | DRC Errors | #ff000018 | xx | ||||
Error: Thermal isolation width…… is less than design rule | (403, 0) | DRC Errors | #50008018 | ++ | ||||
Error: Thermal isolation…… spacing is less than design rule | (403, 1) | DRC Errors | #50008018 | + | ||||
Error: Spacing between thermal…… isolation and metal is less than design rule | (403, 2) | DRC Errors | #50008018 | xx | ||||
Error: Thermal isolation and…… device layer overlap, or spacing is less than design rule | (403, 3) | DRC Errors | #50008018 | x | ||||
Dream Photonics Black Box-Not…… Fabricated | (998, 0) | #00000018 | hollow | |||||
| Errors | (999, 0) | SiEPIC | #0000ff18 | || |
Extrusion specs
| # | Mask | Limits (μm) | Sidewall (°) | Opt. Medium | Elec. Medium |
|---|---|---|---|---|---|
| 0 | () | -inf, 0 | 0 | cSi_Li1993_293K | Si |
| 1 | () | -2, 1.8 | 0 | SiO2_Palik_LowLoss | SiO2 |
| 2 | 'M1_heater'**0.3 | 1.5, 2 | 0 | SiO2_Palik_LowLoss | SiO2 |
| 3 | 'M2_router'**0.3 | 1.5, 4 | 0 | SiO2_Palik_LowLoss | SiO2 |
| 4 | 'M1_heater' | 1.5, 1.7 | 0 | cSi_Li1993_293K | LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=0.2, fit_param={'max_num_poles': 16}) |
| 5 | 'M2_router' | 1.7, 3.7 | 0 | Al_Rakic1995 | LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=38.0, fit_param={'max_num_poles': 16}) |
| 6 | 'M_Open' | 3.7, 4 | 0 | Medium() | Medium() |
| 7 | 'Oxide open to BOX' | 0, inf | 0 | Medium() | Medium() |
| 8 | 'Si' | 0, 0.22 | 0 | cSi_Li1993_293K | Si |
| 9 | 'Si Slab' | 0, 0.09 | 0 | cSi_Li1993_293K | Si |
| 10 | 'Deep Trench' + 'Thermal…… Isolation Trenches' | -inf, inf | 0 | Medium() | Medium() |
Ports
| Name | Classification | Description | Width (μm) | Limits (μm) | Radius (μm) | Modes | Target n_eff | Path profiles (μm) | Voltage path | Current path |
|---|---|---|---|---|---|---|---|---|---|---|
| CPW | electrical | CPW (signal width: 35.0, gap:…… 10.0) | 203 | -200.426, 205.826 | 0 | 1 | auto | 'gnd0'@'M2_router': 75 (-65),…… 'gnd1'@'M2_router': 75 (+65), 'signal'@'M2_router': 35 | (27.5, 2.7) (17.5, 2.7) | (22.5, 8.7) (-22.5, 8.7)…… (-22.5, -3.3) (22.5, -3.3) |
| MM_TE_1550_2000 | optical | Multimode Strip TE 1550 nm,…… w=2000 nm | 6 | -2, 2.22 | 0 | 10 | 3.5 | 'Si': 2 | ||
| MM_TE_1550_3000 | optical | Multimode Strip TE 1550 nm,…… w=3000 nm | 6 | -2, 2.22 | 0 | 15 | 3.5 | 'Si': 3 | ||
| Rib_TE_1310_350 | optical | Rib (90 nm slab) TE 1310 nm,…… w=350 nm | 2.35 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.35, 'Si Slab': 3 | ||
| Rib_TE_1550_500 | optical | Rib (90 nm slab) TE 1550 nm,…… w=500 nm | 2.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.5, 'Si Slab': 3 | ||
| Slot_TE_1550_500 | optical | Slot TE 1550 nm, w=500 nm,…… gap=100nm | 3 | -1, 1.22 | 0 | 1 | 3.5 | 'Si': 0.2 (-0.15), 'Si': 0.2…… (+0.15) | ||
| TE-TM_1550_450 | optical | Strip TE-TM 1550, w=450 nm | 2.2 | -1, 1.22 | 0 | 2 | 3.5 | 'Si': 0.45 | ||
| TE_1310_350 | optical | Strip TE 1310 nm, w=350 nm | 1.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.35 | ||
| TE_1310_410 | optical | Strip TE 1310 nm, w=410 nm | 1.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.41 | ||
| TE_1550_500 | optical | Strip TE 1550 nm, w=500 nm | 1.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.5 | ||
| TM_1310_350 | optical | Strip TM 1310 nm, w=350 nm | 1.5 | -0.6, 0.82 | 0 | 1 + 1 (TM) | 3.5 | 'Si': 0.35 | ||
| TM_1550_500 | optical | Strip TM 1550 nm, w=500 nm | 1.5 | -0.6, 0.82 | 0 | 1 + 1 (TM) | 3.5 | 'Si': 0.5 | ||
| eskid_TE_1550 | optical | eskid TE 1550 | 2 | -0.7, 0.92 | 0 | 1 | 3.5 | 'Si': 0.35, 'Si': 0.06…… (+0.265), 'Si': 0.06 (-0.265), 'Si': 0.06 (+0.385), 'Si': 0.06 (-0.385), 'Si': 0.06 (+0.505), 'Si': 0.06 (-0.505), 'Si': 0.06 (+0.625), 'Si': 0.06 (-0.625) |
Background medium
- Optical: Medium()
- Electrical: Medium()
Solving the CPW mode gives the real impedance the phase shifter presents across the band.
# Calculate RF mode, impedance, and group index for the active phase shifter CPWmode_solver_rf, z0_rf = pf.port_modes( cpw_spec, freqs_rf, mesh_refinement=30, impedance=True, group_index=True)
plt.plot(freqs_rf / 1e9, z0_rf.isel(mode_index=0).real)plt.xlabel("Frequency (GHz)")plt.ylabel("Real Impedance (Ω)")plt.show()Uploading task 'Mode-ModeSolver…'Starting task 'Mode-ModeSolver': https://tidy3d.simulation.cloud/workbench?taskId=mo-3f5a4ea0-cc25-464c-8604-721c3926b0faDownloading data from 'Mode-ModeSolver'…
Design Tip: The RF probe pads themselves act as transmission lines. To ensure a clean signal transition from external RF probes (which are typically 50 Ω) onto the chip, we define a separate, wider CPW geometry for the Ground-Signal-Ground (GSG) pads and verify their impedance.
# Define a CPW port spec for the GSG padspad_size = 100.0pad_gap = 50.0 # (µm) gap distance from signal to groundcpw_spec_pad = pf.cpw_spec( "M2_router", signal_width=pad_size, gap=pad_gap, ground_width=pad_size)tech.add_port("CPW_Pad", cpw_spec_pad)Layers
| Name | Layer | Description | Color | Pattern | Device marker | Conductive | Min. width (μm) | Min. spacing (μm) |
|---|---|---|---|---|---|---|---|---|
| Si | (1, 0) | SiEPIC - Waveguide | #ff80a818 | \\ | ||||
| PinRec | (1, 10) | SiEPIC | #ff80a818 | xx | ||||
| PinRecM | (1, 11) | SiEPIC | #80000018 | + | ||||
| Si Slab | (2, 0) | Dedicated Run Layers - Device…… Layer Partial Etch | #c080ff18 | / | ||||
| Direct Metal | (5, 0) | Dedicated Run Layers | #80a8ff18 | || | ||||
| Oxide open to BOX | (6, 0) | Dedicated Run Layers | #ff000018 | - | ||||
| Text | (10, 0) | Text-Not Fabricated | #00000018 | hollow | ||||
| M1_heater | (11, 0) | TiW Heater | #0000ff18 | \\ | ||||
| M2_router | (12, 0) | TiW/Au Routing Bilayer | #ffbf0018 | // | ||||
| M_Open | (13, 0) | Bond Pad Open | #80005718 | \\ | ||||
| Si n | (20, 0) | Dedicated Run Layers | #afff8018 | - | ||||
| Si p | (21, 0) | Dedicated Run Layers | #ffd9df18 | = | ||||
| Si n+ | (22, 0) | Dedicated Run Layers | #ff800018 | x | ||||
| Si p+ | (23, 0) | Dedicated Run Layers | #ddff0018 | xx | ||||
| Si n++ | (24, 0) | Dedicated Run Layers | #00ffff18 | + | ||||
| Si p++ | (25, 0) | Dedicated Run Layers | #00800018 | ++ | ||||
| ANT Reserved | (31, 0) | SiEPIC/ANT Reserved | #9580ff18 | / | ||||
| ANT Reserved 1 | (33, 0) | ANT Reserved | #9580ff18 | / | ||||
| Via to silicon | (40, 0) | Dedicated Run Layers | #0000ff18 | . | ||||
| DevRec | (68, 0) | SiEPIC | #00800018 | . | ||||
| FbrTgt | (81, 0) | SiEPIC/Dedicated Run Layers | #80808018 | ++ | ||||
| ANT Reserved 2 | (102, 0) | ANT Reserved | #9580ff18 | / | ||||
| ANT Reserved 3 | (110, 0) | ANT Reserved | #9580ff18 | / | ||||
| Custom Dicing | (189, 0) | #00000018 | hollow | |||||
| SEM Imaging | (200, 0) | #ff000018 | x | |||||
| Deep Trench | (201, 0) | #00ff0018 | . | |||||
| Deep Trench Handling Exclusion | (202, 0) | #00760018 | : | |||||
| Thermal Isolation Trenches | (203, 0) | #00800018 | \ | |||||
| Laser Integration Shelf | (205, 0) | Dedicated Run Layers | #69ff0518 | xx | ||||
| Floor Plan-Not Fabricated | (290, 0) | #c080ff18 | hollow | |||||
Error: device layer width is…… less than design rule | (301, 0) | DRC Errors | #80005718 | = | ||||
Error: device layer spacing is…… less than design rule | (301, 1) | DRC Errors | #80005718 | - | ||||
Warning: polygons/paths on…… PinRec layer (1/10) will NOT be fabricated | (301, 2) | DRC Errors | #80005718 | || | ||||
Error: direct metal width is…… less than 5 microns | (305, 0) | DRC Errors | #80808018 | ++ | ||||
Error: direct metal spacing is…… less than 10 microns | (305, 1) | DRC Errors | #80808018 | + | ||||
Error: TiW width is less than 3…… microns | (311, 0) | DRC Errors | #ffa08018 | // | ||||
Error: TiW spacing is less than…… 3 microns | (311, 1) | DRC Errors | #ffa08018 | / | ||||
Error: Al width is less than…… design rule | (312, 0) | DRC Errors | #00ffff18 | | | ||||
Error: Al spacing is less than…… design rule | (312, 1) | DRC Errors | #00ffff18 | // | ||||
Error: Spacing between TiW and…… Al is less than 5 microns | (312, 3) | DRC Errors | #00ffff18 | \\ | ||||
Error: Oxide window width is…… less than 10 microns | (313, 0) | DRC Errors | #01ff6b18 | || | ||||
Error: Oxide window spacing is…… less than 10 microns | (313, 1) | DRC Errors | #01ff6b18 | | | ||||
Error: Oxide window is not…… placed over Al | (313, 2) | DRC Errors | #01ff6b18 | // | ||||
| Standard Design Area | (350, 0) | DRC Errors | #ddff0018 | \ | ||||
Error: Features outside design…… area. Verify design size and centering. | (350, 1) | DRC Errors | #ddff0018 | : | ||||
Error: Dicing lane width is…… less than 100 microns | (389, 0) | DRC Errors | #ff00ff18 | ++ | ||||
Error: Spacing between dicing…… lane and devices is less than 50 microns | (389, 1) | DRC Errors | #ff00ff18 | + | ||||
Error: SEM width is less than…… 500 nm | (400, 0) | DRC Errors | #ff9d9d18 | x | ||||
| Deep Trench Design Area | (401, 0) | DRC Errors | #80a8ff18 | xx | ||||
Error: Metal, SEM, or handling…… region overlap with deep trenches. Verify design centering | (401, 1) | DRC Errors | #80a8ff18 | x | ||||
Warning: Silicon features…… outside deep trench design area. Verify accuracy before submission | (401, 2) | DRC Errors | #80a8ff18 | = | ||||
Error: Spacing between metal…… and deep trench is less than 30 microns | (401, 3) | DRC Errors | #80a8ff18 | - | ||||
Error: Deep trench width is…… less than 260 microns | (401, 4) | DRC Errors | #80a8ff18 | || | ||||
Error: Deep trench handling…… area missing. Please add handling area of size shown by polygons | (402, 0) | DRC Errors | #ff000018 | + | ||||
Error: Features inside deep…… trench handling area | (402, 1) | DRC Errors | #ff000018 | xx | ||||
Error: Thermal isolation width…… is less than design rule | (403, 0) | DRC Errors | #50008018 | ++ | ||||
Error: Thermal isolation…… spacing is less than design rule | (403, 1) | DRC Errors | #50008018 | + | ||||
Error: Spacing between thermal…… isolation and metal is less than design rule | (403, 2) | DRC Errors | #50008018 | xx | ||||
Error: Thermal isolation and…… device layer overlap, or spacing is less than design rule | (403, 3) | DRC Errors | #50008018 | x | ||||
Dream Photonics Black Box-Not…… Fabricated | (998, 0) | #00000018 | hollow | |||||
| Errors | (999, 0) | SiEPIC | #0000ff18 | || |
Extrusion specs
| # | Mask | Limits (μm) | Sidewall (°) | Opt. Medium | Elec. Medium |
|---|---|---|---|---|---|
| 0 | () | -inf, 0 | 0 | cSi_Li1993_293K | Si |
| 1 | () | -2, 1.8 | 0 | SiO2_Palik_LowLoss | SiO2 |
| 2 | 'M1_heater'**0.3 | 1.5, 2 | 0 | SiO2_Palik_LowLoss | SiO2 |
| 3 | 'M2_router'**0.3 | 1.5, 4 | 0 | SiO2_Palik_LowLoss | SiO2 |
| 4 | 'M1_heater' | 1.5, 1.7 | 0 | cSi_Li1993_293K | LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=0.2, fit_param={'max_num_poles': 16}) |
| 5 | 'M2_router' | 1.7, 3.7 | 0 | Al_Rakic1995 | LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=38.0, fit_param={'max_num_poles': 16}) |
| 6 | 'M_Open' | 3.7, 4 | 0 | Medium() | Medium() |
| 7 | 'Oxide open to BOX' | 0, inf | 0 | Medium() | Medium() |
| 8 | 'Si' | 0, 0.22 | 0 | cSi_Li1993_293K | Si |
| 9 | 'Si Slab' | 0, 0.09 | 0 | cSi_Li1993_293K | Si |
| 10 | 'Deep Trench' + 'Thermal…… Isolation Trenches' | -inf, inf | 0 | Medium() | Medium() |
Ports
| Name | Classification | Description | Width (μm) | Limits (μm) | Radius (μm) | Modes | Target n_eff | Path profiles (μm) | Voltage path | Current path |
|---|---|---|---|---|---|---|---|---|---|---|
| CPW | electrical | CPW (signal width: 35.0, gap:…… 10.0) | 203 | -200.426, 205.826 | 0 | 1 | auto | 'gnd0'@'M2_router': 75 (-65),…… 'gnd1'@'M2_router': 75 (+65), 'signal'@'M2_router': 35 | (27.5, 2.7) (17.5, 2.7) | (22.5, 8.7) (-22.5, 8.7)…… (-22.5, -3.3) (22.5, -3.3) |
| CPW_Pad | electrical | CPW (signal width: 100.0, gap:…… 50.0) | 398 | -613.296, 618.696 | 0 | 1 | auto | 'gnd0'@'M2_router': 100 (-150),…… 'gnd1'@'M2_router': 100 (+150), 'signal'@'M2_router': 100 | (100, 2.7) (50, 2.7) | (75, 28.7) (-75, 28.7) (-75,…… -23.3) (75, -23.3) |
| MM_TE_1550_2000 | optical | Multimode Strip TE 1550 nm,…… w=2000 nm | 6 | -2, 2.22 | 0 | 10 | 3.5 | 'Si': 2 | ||
| MM_TE_1550_3000 | optical | Multimode Strip TE 1550 nm,…… w=3000 nm | 6 | -2, 2.22 | 0 | 15 | 3.5 | 'Si': 3 | ||
| Rib_TE_1310_350 | optical | Rib (90 nm slab) TE 1310 nm,…… w=350 nm | 2.35 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.35, 'Si Slab': 3 | ||
| Rib_TE_1550_500 | optical | Rib (90 nm slab) TE 1550 nm,…… w=500 nm | 2.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.5, 'Si Slab': 3 | ||
| Slot_TE_1550_500 | optical | Slot TE 1550 nm, w=500 nm,…… gap=100nm | 3 | -1, 1.22 | 0 | 1 | 3.5 | 'Si': 0.2 (-0.15), 'Si': 0.2…… (+0.15) | ||
| TE-TM_1550_450 | optical | Strip TE-TM 1550, w=450 nm | 2.2 | -1, 1.22 | 0 | 2 | 3.5 | 'Si': 0.45 | ||
| TE_1310_350 | optical | Strip TE 1310 nm, w=350 nm | 1.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.35 | ||
| TE_1310_410 | optical | Strip TE 1310 nm, w=410 nm | 1.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.41 | ||
| TE_1550_500 | optical | Strip TE 1550 nm, w=500 nm | 1.5 | -0.6, 0.82 | 0 | 1 | 3.5 | 'Si': 0.5 | ||
| TM_1310_350 | optical | Strip TM 1310 nm, w=350 nm | 1.5 | -0.6, 0.82 | 0 | 1 + 1 (TM) | 3.5 | 'Si': 0.35 | ||
| TM_1550_500 | optical | Strip TM 1550 nm, w=500 nm | 1.5 | -0.6, 0.82 | 0 | 1 + 1 (TM) | 3.5 | 'Si': 0.5 | ||
| eskid_TE_1550 | optical | eskid TE 1550 | 2 | -0.7, 0.92 | 0 | 1 | 3.5 | 'Si': 0.35, 'Si': 0.06…… (+0.265), 'Si': 0.06 (-0.265), 'Si': 0.06 (+0.385), 'Si': 0.06 (-0.385), 'Si': 0.06 (+0.505), 'Si': 0.06 (-0.505), 'Si': 0.06 (+0.625), 'Si': 0.06 (-0.625) |
Background medium
- Optical: Medium()
- Electrical: Medium()
The pads are much wider than the phase-shifter electrodes, so their impedance is checked separately.
# Calculate impedance for the pad CPW geometrymode_solver_pad, z0_pad = pf.port_modes( cpw_spec_pad, freqs_rf, mesh_refinement=30, impedance=True, group_index=True)
plt.plot(freqs_rf / 1e9, z0_pad.isel(mode_index=0).real)plt.xlabel("Frequency (GHz)")plt.ylabel("Real Impedance (Ω)")plt.show()Uploading task 'Mode-ModeSolver…'Starting task 'Mode-ModeSolver': https://tidy3d.simulation.cloud/workbench?taskId=mo-1385846b-7123-4b10-a512-9b88d139aecfDownloading data from 'Mode-ModeSolver'…
On-Chip Termination Material & Geometry
Section titled “On-Chip Termination Material & Geometry”To prevent electrical back-reflections at the end of the traveling-wave electrode, the line must be terminated with an on-chip load matching the characteristic impedance (50 Ω). In a GSG (Ground-Signal-Ground) layout, this is efficiently achieved using two parallel 100 Ω resistors bridging the center signal trace to each ground trace.
Why use the Heater layer? Standard routing metals (like thick Aluminum or Copper) have high conductivity. If we tried to make a 100 Ω resistor out of standard routing metal, the trace would have to be microscopically narrow or incredibly long! Instead, we use the heater_metal layer (often TiN, TaN, or doped polysilicon). Because this layer is both physically thin () and has a lower conductivity (), it provides a high sheet resistance (), allowing us to hit 100 Ω with a compact, DRC-safe footprint.
# Calculate the required physical width for the 100 ohm termination resistors# We solve the resistance equation R = L / (sigma * W * t) for W.# Here, L is the distance the current travels (pad_gap).
heater_thickness = tech.parametric_kwargs["heater_thickness"]heater_conductivity = tech.parametric_kwargs["heater_metal"]["electrical"].conductivity
target_resistance = 100.0 # (Ohms) Two 100 Ohm resistors in parallel yield 50 Ohms
# Calculate required width (W = L / (R * sigma * t))termination_width = pad_gap / ( target_resistance * heater_conductivity * heater_thickness)
print(f"Required termination width for 100 Ohms: {termination_width:.2f} µm")Required termination width for 100 Ohms: 12.50 µm@pf.parametric_component(name_prefix="PN Phase Shifter")def pn_phase_shifter( *, length=3000, # phase shifter length (µm) n_eff=n_eff_rib, # phase shifter effective index n_group=n_group_rib, # phase shifter group index ref_freq=freq0, # reference frequency for the effective and group indices propagation_loss=5e-4, # phase shifter propagation loss (dB/µm) v_piL=vpi_L_v_cm * 2 * 1e4, # Vpi.L (V.µm) f3db=50e9, # 3 dB bandwidth taper_length=10, # taper length (µm) i_width=0.15, # intrinsic region width (µm) p_width=0.5, # p-doping/n-doping width (µm) pp_width=1.0, # p++ doping/n++ doping width (µm) pad_size=pad_size, # RF pad size (µm) pad_distance=300, # pad distance from shifter (µm) pad_separation=pad_gap + pad_size, # pad separation (signal-ground) (µm) termination_width=termination_width, # Termination width): """ PN junction depletion-mode phase shifter for high-speed modulation.
Architecture: - Rib waveguide (wider than strip for lower loss) - PN junction laterally placed across waveguide - Doping regions: p-contact, N, N++ - Traveling-wave electrode (CPW) for high bandwidth - Dual-arm configuration for push-pull MZM
Physics: - Free-carrier plasma dispersion effect - Reverse-biased PN junction depletes carriers - Phase shift: Δφ ∝ ΔV × L
Specs: - Vπ·L: ~1-2 V·cm - Loss: < 5 dB/cm - Bandwidth: > 50 GHz """ rib_width = tech.ports["Rib_TE_1550_500"].width
# Create strip-to-rib tapers taper = pf.parametric.transition( port_spec1=port_spec, port_spec2=rib, length=taper_length, min_width=0.1 )
# Create straight rib section straight = pf.parametric.straight(port_spec=rib, length=length, name="PS Arm")
# Add virtual electrical port for time domain simulation elec_vir = pf.virtual_port_spec(classification="electrical") straight.add_port(pf.Port((length / 2, 0), -90, spec=elec_vir))
# Create top arm phase modulator mod_model = pf.AnalyticWaveguideModel( n_eff=n_eff, reference_frequency=ref_freq, length=length, propagation_loss=propagation_loss, n_group=n_group, v_piL=v_piL, ) mod_model.time_stepper = pf.PhaseModTimeStepper( length=length, n_eff=n_eff, n_group=n_group, v_piL=v_piL, propagation_loss=propagation_loss, f_3dB=f3db, )
straight.add_model(mod_model)
# Build component c = pf.Component()
# Each arm is a taper in, the rib section, and the same taper reversed on the way out. # The rib ports are named because its electrical port E0 would otherwise come first. arm_parts = {"taper": taper, "rib": straight} arm_sequence = "taper - P0:rib:P1 - P1:taper"
# Add first arm (top) arm = pf.reference_chain(arm_parts, arm_sequence) c.add(*arm) ps = arm[1]
c.add_port([arm[0]["P0"], arm[-1]["P0"]])
# Add doping regions to straight section doping_regions = [ # (center, width, layer) ((i_width + p_width) / 2, p_width, "Si n"), # N-doping ((i_width + pp_width) / 2 + p_width, pp_width, "Si n++"), # N++ doping (-(i_width + p_width) / 2, p_width, "Si p"), # P-doping (-(i_width + pp_width) / 2 - p_width, pp_width, "Si p++"), # P++ doping ]
for y, width, layer in doping_regions: straight.add( layer, pf.Rectangle(center=(length / 2, y), size=(length, width)), # pf.Rectangle(corner2=(length, -y), size=(length, width)), )
# Add CPW transmission line w1, off1, _ = cpw_spec.path_profiles["gnd1"] w0, off0, _ = cpw_spec.path_profiles["signal"] offset = (w0 / 2 + off1 - w1 / 2) / 2
cpw = pf.parametric.straight(port_spec=cpw_spec, length=length + 2 * taper_length)
cpw_ref = pf.Reference(cpw, (0, -offset)) c.add(cpw_ref)
# Add second arm (bottom, for push-pull operation) arm_2 = pf.reference_chain(arm_parts, arm_sequence) for ref in arm_2: ref.translate((0, -2 * offset)) c.add(*arm_2) ps_2 = arm_2[1]
c.add_port([arm_2[0]["P0"], arm_2[-1]["P0"], ps["E0"], ps_2["E0"]])
# Add RF terminals (pads) c.add_terminal( { f"{name}:{side}": pf.Terminal( "M2_router", pf.Rectangle(center=(x, y - offset), size=(pad_size, pad_size)), ) for x, side in [ (-pad_distance - pad_size / 2, "in"), (length + 2 * taper_length + pad_distance + pad_size / 2, "out"), ] for y, name in [ (-pad_separation, "gnd0"), (0, "signal"), (pad_separation, "gnd1"), ] } )
# Route from pads to CPW for p, side in [("E0", "in"), ("E1", "out")]: for name in ("gnd0", "signal", "gnd1"): c.add( pf.parametric.route_taper( terminal1=c[f"{name}:{side}"], terminal2=cpw_ref[p].terminals(name) ) )
# Add integrated termination at input c.add( "M1_heater", pf.Path(c["gnd0:in"].center(), termination_width).segment( c["gnd1:in"].center() ), )
# Add circuit model c.add_model(pf.CircuitModel(), "Circuit")
# Removing cpw models and ports to avoid warnings and errors because this component is not involved in circuit simulation cpw.remove_model(list(cpw.models.keys())[0]) for port in list(cpw.ports.keys()): cpw.remove_port(port)
return c
ps = pn_phase_shifter(length=ps_length)viewer(ps)Mach-Zehnder Modulator (MZM) Test Assembly
Section titled “Mach-Zehnder Modulator (MZM) Test Assembly”To validate the calculated phase shifter length and the resulting extinction ratio, we assemble a test MZM using a virtual netlist. This configuration pairs the Y-junction splitters with the dual-arm PN phase shifter to simulate the full interference-based intensity modulation. This “virtual” assembly allows for rapid DC verification of the modulation depth before proceeding to the physical routing of the high-speed electrodes.
# Define the Netlist for the Mach-Zehnder Modulatornetlist_mzm = { "name": "MZM_PushPull_Virtual", "instances": { "SPLITTER": {"component": splitter, "origin": (-100, -15)}, "ARM": ps, "COMBINER": { "component": splitter, "origin": (ps_length + 100, -15), "rotation": 180, }, }, # using "virtual_connections" allows logical linking without physical routing "virtual_connections": [ # Splitter Output 2 -> Top Arm Input (("SPLITTER", "P2"), ("ARM", "P0")), # Splitter Output 1 -> Bottom Arm Input (("SPLITTER", "P1"), ("ARM", "P2")), # Top Arm Output -> Combiner Input 1 (("ARM", "P1"), ("COMBINER", "P1")), # Bottom Arm Output -> Combiner Input 2 (("ARM", "P3"), ("COMBINER", "P2")), ], "ports": [ ("SPLITTER", "P0"), # Optical Input ("COMBINER", "P0"), # Optical Output ], "models": [pf.CircuitModel()],}
# Create component from netlistmzm_device = pf.component_from_netlist(netlist_mzm)
viewer(mzm_device)DC Verification: Extinction Ratio
Here we verify the design by simulating the static transmission at different voltage states.
We drive the MZM with the design voltage swing of 2 Vₚₚ (toggling between -1 V and +1 V relative to the bias).
Observation:
The plot below confirms that this voltage swing results in an Extinction Ratio (ER) of ~3 dB, successfully validating the length calculation performed in the previous step.
v_bias = 7voltages = [-1, 0, 1]
plt.figure()for v_app in voltages: # Update dictionary based on your previous cell structure updates = { (ps.name, 0, "PS Arm", 0): {"model_updates": {"voltage": v_bias + v_app}}, (ps.name, 0, "PS Arm", 1): {"model_updates": {"voltage": -v_app}}, }
# Compute S-matrix and extract Transmission (P0 -> P1) S = mzm_device.s_matrix(freqs, model_kwargs={"updates": updates}) T_dB = 20 * np.log10(np.abs(S["P0@0", "P1@0"]))
plt.plot(wavelengths, T_dB, label=f"V_app={v_app}V")
plt.xlabel("Wavelength (µm)")plt.ylabel("Transmission (dB)")plt.legend()plt.grid(True)14:35:18 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-94e16d2f-20a 9-4157-8bb2-b0bffdcdd245'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-fa4754b6-fce 1-4baf-8c15-d6d1414da3de'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-78ae519b-97bd- 4133-b29d-29f14e70a5ec'.
Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=mo-66aa27c6-b7ec- 45d2-9034-87621b9afe66'.

Waveguide Crossing
Section titled “Waveguide Crossing”Waveguide crossings are essential for complex routing in high-density PICs, allowing signal paths to intersect with minimal interference.
Reference:
Y. Ma et al., “Ultralow loss single layer submicron silicon waveguide crossing for SOI optical interconnect,” Opt Express, 2023 21 (24), 29374–29382, doi: 10.1364/OE.21.029374
Design Tip:
The Challenge: A direct waveguide intersection causes high loss and crosstalk because light diffracts (spreads out) as it crosses the unguided gap.
The Solution: We widen the waveguide before the crossing. A larger optical mode has a narrower divergence angle (), allowing it to “jump” the gap with minimal scattering.
Optimization: A linear taper is often too long. To minimize the footprint, we use an optimization algorithm (e.g., particle swarm optimization) to find a cubic-spline width profile. This algorithm determines the fastest possible rate of expansion that remains adiabatic, meaning the mode evolves smoothly without scattering energy into higher-order modes.
from scipy.interpolate import make_interp_spline
default_widths = ( 0.5, 0.6, 0.95, 1.32, 1.44, 1.46, 1.466, 1.52, 1.58, 1.62, 1.76, 2.15, 0.5,)
@pf.parametric_component(name_prefix="Crossing")def adiabatic_crossing( *, arm_length=4.5, # Length of crossing arms (µm) widths=default_widths,): """ Create a 4-arm adiabatic crossing with smooth cubic taper profiles.
Parameters: arm_length (float): Length of the main arm (µm). widths (Sequence[float]): Target waveguide widths (µm) along each arm.
Returns: A crossing component. """ # Number of points used to discretize the cubic spline profile. num_points = int(arm_length / pf.config.tolerance)
wg_width, _ = port_spec.path_profile_for("Si") # extract initial waveguide width
# Create cubic spline interpolation for widths coords = np.linspace(0, arm_length, len(widths)) spline = make_interp_spline(coords, widths[::-1], k=3)
# Pre-compute widths and positions from the interpolation coords = np.linspace(0, arm_length, num_points) widths = spline(coords)
arm1 = pf.Path((0, 0), wg_width) for x, w in zip(coords[1:], widths[1:]): arm1.segment((x, 0), w)
arm2 = arm1.copy().rotate(90) arm3 = arm1.copy().rotate(180) arm4 = arm1.copy().rotate(270)
c = pf.Component() c.add("Si", *pf.boolean([arm1, arm2], [arm3, arm4], "+"))
# Add ports c.add_port(c.detect_ports([port_spec])) assert len(c.ports) == 4, "Port detection failed: expected exactly 4 ports."
# Add Tidy3D simulation model with symmetry conditions c.add_model( pf.Tidy3DModel( port_symmetries=[ ("P1", "P0", "P3", "P2"), # symmetry about x-axis ("P2", "P3", "P0", "P1"), # symmetry about y-axis ("P3", "P2", "P1", "P0"), # inversion symmetry ], ), "Tidy3D", ) return c
crossing = adiabatic_crossing()viewer(crossing)# Compute the scattering matrix of the crossing over the frequency ranges_matrix_crossing = crossing.s_matrix(freqs)# Plot the magnitude of the S-matrix to evaluate transmission and crosstalk_ = pf.plot_s_matrix(s_matrix_crossing, input_ports=["P0"])# Add the simulation results as a DataModel to avoid future FDTD simulations for different frequencies# crossing.add_model(pf.DataModel(s_matrix_crossing))14:35:22 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-31cdd096-751 5-4e8f-a6be-462bf11f483f'.

Optical Termination
Section titled “Optical Termination”This section defines a low-reflection waveguide termination used to suppress back-reflections at open waveguide ends.
Design Tip: To prevent destabilizing back-reflections (return loss), avoid abrupt waveguide cuts. Instead, use an adiabatic tapered termination.
By gradually narrowing the waveguide to a sharp tip (e.g., < 100 nm), the optical mode is forced to expand and radiate smoothly into the cladding (“mode deconfinement”). This “squeezes” the light out without creating a reflective facet, typically achieving reflections lower than -30 dB.
# Load and visualize a low-reflection waveguide terminator PCelltermination = siepic.component("ebeam_terminator_te1550")viewer(termination)Simulation shows very low return loss (< -45 dB).
# Simulate and plot the terminator S-matrixs_matrix_termination = termination.s_matrix(freqs)_ = pf.plot_s_matrix(s_matrix_termination, y="dB", input_ports=["P0"])# Add the simulation results as a DataModel to avoid future FDTD simulations for different frequencies# termination.add_model(pf.DataModel(s_matrix_termination))14:35:23 -03 Loading simulation from local cache. View cached task using web UI at 'https://tidy3d.simulation.cloud/workbench?taskId=fdve-2228c8c5-ab3 3-4fd2-a674-02782a625875'.

Bond Pads
Section titled “Bond Pads”Bond pads are the primary electrical interface between the photonic chip and the external environment. These large metal areas (typically 100 × 100 µm) are designed for wire bonding or flip-chip packaging, providing reliable electrical connections for DC biasing of heaters and high-speed RF signaling for modulators. The design ensures low contact resistance and sufficient surface area to accommodate standard packaging tolerances.
bp = pf.Component("BondPad")bp.add_terminal(pf.Terminal("M2_router", pf.Rectangle(size=(100, 100))), add_structure=True)bp.add("M_Open", pf.Rectangle(size=(95, 95)))viewer(bp)