Skip to content

Custom Component Library

Download notebook (.zip)

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

Load the technology.

# Load the technology stack and helper imports
from 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 notebook
td.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 LiveViewer
from photonforge.stencil import as_component
# Suppress warnings about unmatched terminals and ports
warnings.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 definition
pf.config.default_kwargs["port_spec"] = port_spec
pf.config.default_kwargs["radius"] = 5 # default bend radius (µm)
pf.config.default_kwargs["euler_fraction"] = 0.5
pf.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 (λ0\lambda_0, f0f_0) 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 / wavelengths
freq0 = pf.C_0 / lambda0
# Waveguide properties
propagation_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 frequency
mode_solver = pf.port_modes(port_spec, [freq0], mesh_refinement=50, group_index=True)
# Plot the mode electric-field profile
mode_solver.plot_field("E", mode_index=0, f=freq0)
# Extract the fundamental-mode effective and group indices
n_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-2918b66e7b00
Downloading 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 frequency
mode_solver_rib = pf.port_modes(rib, [freq0], mesh_refinement=50, group_index=True)
# Plot the mode electric-field profile
mode_solver_rib.plot_field("E")
# Extract the fundamental-mode group index
n_group_rib = mode_solver_rib.data.n_group.isel(mode_index=0).item()
# Extract the fundamental-mode effective index
n_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-a6c5b21cc7fb
Downloading data from 'Mode-ModeSolver'…

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 component
angle_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 * 180
print(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 geometry
viewer(edge_coupler)
Taper angle: 10.282162084410936°

# Simulate and plot the edge-coupler S-matrix
s_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-05466ec46792
Starting task 'P1@0': https://tidy3d.simulation.cloud/workbench?taskId=fdve-fd4b11e4-4232-45ae-9c99-944fac6ad19a
Downloading data from 'P0@0'…
Downloading data from 'P1@0'…

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)
Layout
Schematic
# Simulate and plot the power monitor S-matrix
s_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-5758d4e8b985
Downloading data from 'Mode-StripTE1550nmw500nm'…

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)
Layout
Schematic

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 PCell
splitter = siepic.component("ebeam_y_1550")
viewer(splitter)

# Simulate and plot the splitter S-matrix
s_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'.                                         

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 component

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:

κint=12κext2\kappa_{\text{int}} = \frac{1}{2} \kappa_{\text{ext}}^2

where:

  • κext\kappa_{\text{ext}}: bus-to-ring (external) power coupling coefficient (κext≈0.2\kappa_{\text{ext}} \approx 0.2 in our design)
  • κint\kappa_{\text{int}}: ring-to-ring (internal) power coupling coefficient (κint≈0.02\kappa_{\text{int}} \approx 0.02 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 response
s_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")]) ** 2
p2 = np.abs(s_matrix_rr[("P0@0", "P2@0")]) ** 2
print(
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-351cc62f5efd
Downloading 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")]) ** 2
p2 = np.abs(s_matrix_br[("P0@0", "P2@0")]) ** 2
print(
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)
Layout
Schematic
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'.                                           

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:

  1. Free Spectral Range (FSR): The frequency spacing between consecutive resonances is determined by the group index (ngn_g) and total ring perimeter (LL):

    FSR=λ2ngLFSR = \frac{\lambda^2}{n_g L}
  2. Channel Shift: We need to shift the resonance by 1/4 FSR1/4 \text{ FSR}, which corresponds to a phase shift of π/2\pi/2. The required change in ring perimeter (ΔL\Delta L) is:

    ΔLperimeter=λ4neff\Delta L_{perimeter} = \frac{\lambda}{4 n_{eff}}

    Since our parameterized coupling_length adds to the ring twice (once per side), the required added_length parameter is:

    added_length=λ8neff\text{added\_length} = \frac{\lambda}{8 n_{eff}}
L_ring = 2 * np.pi * 5 + 2 * 9 # approximate perimeter (µm)
# Compute the FSR and required resonance shift
fsr_nm = (lambda0**2) / (n_group * L_ring) * 1000
added_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 nm
Required 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)
Layout
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'.                                         

@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 S21S_{21} parameter, we can estimate the phase shift Δϕ\Delta\phi 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 tuning
heater.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()

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 (VppV_{pp}), 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:

  1. Target Phase Shift (ϕ\phi): For a push-pull MZM biased at quadrature, the ER is determined by the peak phase excursion ϕpeak\phi_{peak}:

    sin⁡(ϕpeak)=ERlinear−1ERlinear+1\sin(\phi_{peak}) = \frac{ER_{linear} - 1}{ER_{linear} + 1}
  2. Required VπV_{\pi}: We calculate how efficient the modulator needs to be to achieve that ϕpeak\phi_{peak} given your drive voltage VpeakV_{peak} (where Vpeak=Vpp/2V_{peak} = V_{pp}/2):

    Vπ,required=π⋅VpeakϕpeakV_{\pi, required} = \frac{\pi \cdot V_{peak}}{\phi_{peak}}
  3. Required Length (LL): Finally, we use the modulator’s efficiency metric, VπLpush−pullV_{\pi}L_{push-pull} to find the physical length:

    L=VπLpush−pullVπ,requiredL = \frac{V_{\pi}L_{push-pull}}{V_{\pi, required}}
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 # dB
vpi_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 VπLV_{\pi}L 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., VπLarm=2×VπLpush-pullV_{\pi}L_{arm} = 2 \times V_{\pi}L_{push\text{-}pull}).

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 electrodes
freqs_rf = np.linspace(2, 30, 11) * 1e9
cpw_spec = pf.cpw_spec("M2_router", signal_width=35.0, gap=10.0, ground_width=75)
tech.add_port("CPW", cpw_spec)
SiEPIC EBeam Si
version 1.2.2
Layers
NameLayerDescriptionColorPatternDevice markerConductiveMin. width (μm)Min. spacing (μm)
Si(1, 0)SiEPIC - Waveguide#ff80a818\\
PinRec(1, 10)SiEPIC#ff80a818xx
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#00000018hollow
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#ff800018x
Si p+(23, 0)Dedicated Run Layers#ddff0018xx
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)#00000018hollow
SEM Imaging(200, 0)#ff000018x
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#69ff0518xx
Floor Plan-Not Fabricated(290, 0)#c080ff18hollow
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#ff9d9d18x
Deep Trench Design Area(401, 0)DRC Errors#80a8ff18xx
Error: Metal, SEM, or handling…… region overlap with deep trenches. Verify design centering
(401, 1)DRC Errors#80a8ff18x
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#ff000018xx
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#50008018xx
Error: Thermal isolation and…… device layer overlap, or spacing is less than design rule
(403, 3)DRC Errors#50008018x
Dream Photonics Black Box-Not…… Fabricated
(998, 0)#00000018hollow
Errors(999, 0)SiEPIC#0000ff18||
Extrusion specs
#MaskLimits (μm)Sidewall (°)Opt. MediumElec. Medium
0()-inf, 00cSi_Li1993_293KSi
1()-2, 1.80SiO2_Palik_LowLossSiO2
2'M1_heater'**0.31.5, 20SiO2_Palik_LowLossSiO2
3'M2_router'**0.31.5, 40SiO2_Palik_LowLossSiO2
4'M1_heater'1.5, 1.70cSi_Li1993_293K
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=0.2, fit_param={'max_num_poles': 16})
5'M2_router'1.7, 3.70Al_Rakic1995
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=38.0, fit_param={'max_num_poles': 16})
6'M_Open'3.7, 40Medium()Medium()
7'Oxide open to BOX'0, inf0Medium()Medium()
8'Si'0, 0.220cSi_Li1993_293KSi
9'Si Slab'0, 0.090cSi_Li1993_293KSi
10
'Deep Trench' + 'Thermal…… Isolation Trenches'
-inf, inf0Medium()Medium()
Ports
NameClassificationDescriptionWidth (μm)Limits (μm)Radius (μm)ModesTarget n_effPath profiles (μm)Voltage pathCurrent path
CPWelectrical
CPW (signal width: 35.0, gap:…… 10.0)
203-200.426, 205.82601auto
'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_2000optical
Multimode Strip TE 1550 nm,…… w=2000 nm
6-2, 2.220103.5'Si': 2
MM_TE_1550_3000optical
Multimode Strip TE 1550 nm,…… w=3000 nm
6-2, 2.220153.5'Si': 3
Rib_TE_1310_350optical
Rib (90 nm slab) TE 1310 nm,…… w=350 nm
2.35-0.6, 0.82013.5'Si': 0.35, 'Si Slab': 3
Rib_TE_1550_500optical
Rib (90 nm slab) TE 1550 nm,…… w=500 nm
2.5-0.6, 0.82013.5'Si': 0.5, 'Si Slab': 3
Slot_TE_1550_500optical
Slot TE 1550 nm, w=500 nm,…… gap=100nm
3-1, 1.22013.5
'Si': 0.2 (-0.15), 'Si': 0.2…… (+0.15)
TE-TM_1550_450opticalStrip TE-TM 1550, w=450 nm2.2-1, 1.22023.5'Si': 0.45
TE_1310_350opticalStrip TE 1310 nm, w=350 nm1.5-0.6, 0.82013.5'Si': 0.35
TE_1310_410opticalStrip TE 1310 nm, w=410 nm1.5-0.6, 0.82013.5'Si': 0.41
TE_1550_500opticalStrip TE 1550 nm, w=500 nm1.5-0.6, 0.82013.5'Si': 0.5
TM_1310_350opticalStrip TM 1310 nm, w=350 nm1.5-0.6, 0.8201 + 1 (TM)3.5'Si': 0.35
TM_1550_500opticalStrip TM 1550 nm, w=500 nm1.5-0.6, 0.8201 + 1 (TM)3.5'Si': 0.5
eskid_TE_1550opticaleskid TE 15502-0.7, 0.92013.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()
Connected layers: []

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 CPW
mode_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-721c3926b0fa
Downloading 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 pads
pad_size = 100.0
pad_gap = 50.0 # (µm) gap distance from signal to ground
cpw_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)
SiEPIC EBeam Si
version 1.2.2
Layers
NameLayerDescriptionColorPatternDevice markerConductiveMin. width (μm)Min. spacing (μm)
Si(1, 0)SiEPIC - Waveguide#ff80a818\\
PinRec(1, 10)SiEPIC#ff80a818xx
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#00000018hollow
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#ff800018x
Si p+(23, 0)Dedicated Run Layers#ddff0018xx
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)#00000018hollow
SEM Imaging(200, 0)#ff000018x
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#69ff0518xx
Floor Plan-Not Fabricated(290, 0)#c080ff18hollow
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#ff9d9d18x
Deep Trench Design Area(401, 0)DRC Errors#80a8ff18xx
Error: Metal, SEM, or handling…… region overlap with deep trenches. Verify design centering
(401, 1)DRC Errors#80a8ff18x
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#ff000018xx
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#50008018xx
Error: Thermal isolation and…… device layer overlap, or spacing is less than design rule
(403, 3)DRC Errors#50008018x
Dream Photonics Black Box-Not…… Fabricated
(998, 0)#00000018hollow
Errors(999, 0)SiEPIC#0000ff18||
Extrusion specs
#MaskLimits (μm)Sidewall (°)Opt. MediumElec. Medium
0()-inf, 00cSi_Li1993_293KSi
1()-2, 1.80SiO2_Palik_LowLossSiO2
2'M1_heater'**0.31.5, 20SiO2_Palik_LowLossSiO2
3'M2_router'**0.31.5, 40SiO2_Palik_LowLossSiO2
4'M1_heater'1.5, 1.70cSi_Li1993_293K
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=0.2, fit_param={'max_num_poles': 16})
5'M2_router'1.7, 3.70Al_Rakic1995
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=38.0, fit_param={'max_num_poles': 16})
6'M_Open'3.7, 40Medium()Medium()
7'Oxide open to BOX'0, inf0Medium()Medium()
8'Si'0, 0.220cSi_Li1993_293KSi
9'Si Slab'0, 0.090cSi_Li1993_293KSi
10
'Deep Trench' + 'Thermal…… Isolation Trenches'
-inf, inf0Medium()Medium()
Ports
NameClassificationDescriptionWidth (μm)Limits (μm)Radius (μm)ModesTarget n_effPath profiles (μm)Voltage pathCurrent path
CPWelectrical
CPW (signal width: 35.0, gap:…… 10.0)
203-200.426, 205.82601auto
'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_Padelectrical
CPW (signal width: 100.0, gap:…… 50.0)
398-613.296, 618.69601auto
'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_2000optical
Multimode Strip TE 1550 nm,…… w=2000 nm
6-2, 2.220103.5'Si': 2
MM_TE_1550_3000optical
Multimode Strip TE 1550 nm,…… w=3000 nm
6-2, 2.220153.5'Si': 3
Rib_TE_1310_350optical
Rib (90 nm slab) TE 1310 nm,…… w=350 nm
2.35-0.6, 0.82013.5'Si': 0.35, 'Si Slab': 3
Rib_TE_1550_500optical
Rib (90 nm slab) TE 1550 nm,…… w=500 nm
2.5-0.6, 0.82013.5'Si': 0.5, 'Si Slab': 3
Slot_TE_1550_500optical
Slot TE 1550 nm, w=500 nm,…… gap=100nm
3-1, 1.22013.5
'Si': 0.2 (-0.15), 'Si': 0.2…… (+0.15)
TE-TM_1550_450opticalStrip TE-TM 1550, w=450 nm2.2-1, 1.22023.5'Si': 0.45
TE_1310_350opticalStrip TE 1310 nm, w=350 nm1.5-0.6, 0.82013.5'Si': 0.35
TE_1310_410opticalStrip TE 1310 nm, w=410 nm1.5-0.6, 0.82013.5'Si': 0.41
TE_1550_500opticalStrip TE 1550 nm, w=500 nm1.5-0.6, 0.82013.5'Si': 0.5
TM_1310_350opticalStrip TM 1310 nm, w=350 nm1.5-0.6, 0.8201 + 1 (TM)3.5'Si': 0.35
TM_1550_500opticalStrip TM 1550 nm, w=500 nm1.5-0.6, 0.8201 + 1 (TM)3.5'Si': 0.5
eskid_TE_1550opticaleskid TE 15502-0.7, 0.92013.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()
Connected layers: []

The pads are much wider than the phase-shifter electrodes, so their impedance is checked separately.

# Calculate impedance for the pad CPW geometry
mode_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-9b88d139aecf
Downloading data from 'Mode-ModeSolver'…

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 (tt) and has a lower conductivity (σ\sigma), it provides a high sheet resistance (Rs=1σtR_s = \frac{1}{\sigma t}), 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)
Layout

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 Modulator
netlist_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 netlist
mzm_device = pf.component_from_netlist(netlist_mzm)
viewer(mzm_device)
Layout
Schematic

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 = 7
voltages = [-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 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 (θ∝λ/w\theta \propto \lambda/w), 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 range
s_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'.                                         

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 PCell
termination = siepic.component("ebeam_terminator_te1550")
viewer(termination)

Simulation shows very low return loss (< -45 dB).

# Simulate and plot the terminator S-matrix
s_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 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)