Quick Start II: Passive

2b4f4b75236641e1adaa3a6906c4bcf8

This quick start takes you from a foundry PDK to a simulated photonic circuit in five steps:

  1. Load a foundry PDK and inspect its technology.

  2. Import a component from a GDS file and make it simulation-ready by adding ports and a simulation model.

  3. Convert the 2D layout to 3D and compute its S parameters with a cloud FDTD simulation.

  4. Assemble a Mach-Zehnder interferometer (MZI) using PhotonForge’s routing utilities.

  5. Simulate the full circuit and verify the interference fringes against theory.

To follow along you only need PhotonForge and the open-source SiEPIC EBeam PDK:

pip install photonforge siepic-forge

Step 1: Loading a foundry PDK

Foundry PDKs are loaded as PhotonForge technologies. A technology bundles everything the foundry process defines: layers, the layer extrusion recipe used to build 3D simulations, and the standard waveguide cross-sections (port specifications). Here we load the SiEPIC EBeam technology and set it as the default for this session.

[1]:
import numpy as np
import photonforge as pf
import siepic_forge as siepic_pdk
from matplotlib import pyplot as plt

technology = siepic_pdk.ebeam()
pf.config.default_technology = technology
technology
[1]:
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, 2.50SiO2_Palik_LowLossSiO2
2'M1_heater'**0.32.2, 2.70SiO2_Palik_LowLossSiO2
3'M2_router'**0.32.2, 3.30SiO2_Palik_LowLossSiO2
4'M1_heater'2.2, 2.40W_Werner2009
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=1.6, fit_param={'max_num_poles': 16})
5'M2_router'2.4, 30Au_Olmon2012evaporated
LossyMetalMedium……(frequency_range=(100000000.0, 200000000000.0), conductivity=17.0, fit_param={'max_num_poles': 16})
6'M_Open'3, 3.30Medium()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)
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 technology also carries fabrication statistics from the foundry, which enable Monte Carlo yield analysis:

[2]:
technology.random_variables
[2]:
[RandomVariable('si_thickness', **{'value': 0.22, 'stdev': 0.0037166666666666667})]

We can plot the technology’s layer stack in cross-section to see exactly what the foundry process looks like - the silicon device layer over the buried oxide, the heater metal, and the routing metal levels:

[3]:
siepic_pdk.plot_cross_section(technology)
plt.show()
../_images/quick_start_passive_6_0.png

Step 2: Loading a GDS layout

Components can come from the PDK library, from parametric generators, or from any GDS file. Here we import a compact Y splitter layout. Download y_splitter.gds and place it next to this notebook. We use it exactly as you would use a layout received from a colleague or another tool.

[4]:
y_splitter = pf.load_layout("y_splitter.gds")["y_splitter"]
y_splitter
[4]:
../_images/quick_start_passive_8_0.svg

The imported component contains only geometry - no ports and no models. These are the two ingredients that make a layout simulation-ready:

  • Ports define where light enters and exits, and which waveguide cross-section (mode) is used there.

  • A model defines how the component is simulated.

PhotonForge can detect ports automatically from the geometry: we search the component boundaries for cross-sections that match the strip waveguide specification. (Ports can also be placed manually with Port when more control is needed.)

[5]:
y_splitter.add_port(y_splitter.detect_ports(["TE_1550_500"], on_boundary="x"))
y_splitter
[5]:
../_images/quick_start_passive_10_0.svg

Finally we attach a Tidy3DModel, which tells PhotonForge to compute this component’s S parameters with a full 3D FDTD simulation in the cloud. Attaching a model takes no configuration: the defaults come from the component and its technology.

[6]:
y_splitter.add_model(pf.Tidy3DModel())
y_splitter.active_model
[6]:
Tidy3DModel(run_time=None, medium=None, symmetry=(0, 0, 0), boundary_spec=None, monitors=(), structures=(), grid_spec=None, shutoff=None, subpixel=None, courant=None, port_symmetries=(), bounds=((None, None, None), (None, None, None)), source_gap=None, simulation_updates=None, autograd_config=None, verbose=True)

Step 3: Convert 2D layouts to 3D for electromagnetic simulation

PhotonForge automatically extrudes the 2D layout into a full 3D structure using the technology’s extrusion specification - no manual 3D modeling required. We can inspect exactly what the FDTD solver will see - here, a top view of the extruded permittivity at the waveguide core:

[7]:
pf.tidy3d_plot(y_splitter, z=0.11)
plt.show()
../_images/quick_start_passive_14_0.png

Now we compute the S parameters. The simulation runs in the cloud and the result is cached, so re-running this cell (or reusing this component inside a larger circuit) does not re-simulate:

[8]:
wavelengths = np.linspace(1.53, 1.57, 201)
freqs = pf.C_0 / wavelengths

s_matrix = y_splitter.s_matrix(freqs)
Starting…
15:30:21 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=fdve-f5c2af14-c40
             5-481a-b1e0-0d94f333b7e1'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=fdve-314dc7ff-a4f
             a-4793-b8a0-1a7d1ba83653'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=fdve-8c8d9b09-4de
             e-46de-a346-c6690d659700'.
Progress: 100%
[9]:
pf.plot_s_matrix(s_matrix, y="dB", input_ports=["P0"])
plt.show()
../_images/quick_start_passive_17_0.png

The splitter transmits close to the ideal -3 dB into each output arm with low reflection across the full band.

Step 4: Building an MZI with routing

With a verified splitter in hand, we build an unbalanced Mach-Zehnder interferometer: the same Y splitter is used twice (as splitter and combiner), and both arms are created in a single call to route_auto, PhotonForge’s automatic, obstacle-aware router.

What makes the interferometer unbalanced is the path-length difference between its arms, and we state that as an intent rather than drawing it by hand. A PathLengthMatching group tells the router what to equalize; it then splices displacement-neutral meanders into the routes until the targets are met, leaving the endpoints untouched. Matching equalizes offset + routed length, so crediting the first arm with 80 um drives the second one to be 80 um longer.

The same mechanism matches arms to each other (the default), to an absolute target, or to a ladder of per-net targets, and it can equalize geometric length, phase, or group delay.

[10]:
mzi = pf.Component("mzi")

splitter = mzi.add_reference(y_splitter)
combiner = mzi.add_reference(y_splitter)
combiner.rotate(180)
combiner.translate((80, 0))

radius = 5  # bend radius in um; tight enough that a meander fits in a 70 um arm
target_delta_l = 80.0  # extra path length wanted in the long arm (um)

# Both arms in one call. The matching group carries the intent: crediting the first
# net with 80 um makes the router drive the second one 80 um longer.
arms = pf.parametric.route_auto(
    port1=[(splitter, "P1"), (splitter, "P2")],
    port2=[(combiner, "P2"), (combiner, "P1")],
    radius=radius,
    grid_size=1.0,  # finer search grid gives a cleaner meander
    matching=[pf.PathLengthMatching(offsets=[target_delta_l, 0.0])],
)

mzi.add_reference(arms)
mzi
[10]:
Layout
Schematic
Netlist

The router reports what it achieved, and route_length confirms it on each arm. Note that the matcher needs somewhere to put the meander: if no leg of a route is long enough to host it, the pass refuses, leaves the geometry untouched, and says so, rather than returning a layout that quietly misses its target. That is what sets the bend radius here. A meander is four bends, so a wider one needs a longer straight leg than these 70 um arms can offer.

[11]:
lengths = [pf.route_length(route.component) for route in arms.references]

delta_l = max(lengths) - min(lengths)

print(f"Short arm: {min(lengths):.2f} um")
print(f"Long arm:  {max(lengths):.2f} um")
print(f"Path imbalance: {delta_l:.2f} um")
Short arm: 69.50 um
Long arm:  149.50 um
Path imbalance: 80.00 um

Step 5: Circuit simulation

To simulate the MZI we expose its input and output ports and attach a CircuitModel.

A circuit model does not simulate any physics of its own. When you ask this MZI for its S matrix, the circuit model:

  1. walks down the component hierarchy and finds every sub-component, here two Y splitter references and the two routed arms,

  2. asks each one for its own S matrix, which invokes whatever model that sub-component carries,

  3. reads the connectivity from the layout, that is, from which ports actually meet, and

  4. cascades those S matrices into a single response for the whole circuit.

Two consequences are worth noticing. First, models mix freely: the Y splitter answers from its cached FDTD result, one cloud simulation reused for both references, while the routed arms answer from fast analytic waveguide models. Full-wave accuracy goes where it matters and everything else stays instant.

Second, there is no netlist to maintain. The connectivity comes from the layout, so moving a component or rerouting an arm changes the next circuit simulation automatically, and the layout and the system-level prediction cannot drift apart.

[12]:
mzi.add_port([splitter["P0"], combiner["P0"]])
mzi.add_model(pf.CircuitModel())

mzi_s = mzi.s_matrix(freqs)
Starting…
15:30:22 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-b6f390a9-837e-
             44de-bdff-8bfe47144eba'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-00296ff7-d10a-
             43c0-b6a6-120e178da8c4'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-cb3ab898-db6a-
             4914-8437-2bb04cfbf0a2'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-b99a69fe-e92d-
             4e1f-a87c-36250f1e6896'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-925590bf-97fe-
             4ce7-b920-6a3f7d9ea284'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-193386b2-eac5-
             4e1a-8397-24c70148b965'.
Progress: 100%
[13]:
transmission = np.abs(mzi_s[("P0@0", "P1@0")]) ** 2

plt.figure()
plt.plot(wavelengths, 10 * np.log10(transmission))
plt.xlabel("Wavelength (um)")
plt.ylabel("Transmission (dB)")
plt.title("Unbalanced MZI transmission")
plt.ylim(-40, 3)
plt.grid(True, alpha=0.3)
plt.show()
../_images/quick_start_passive_24_0.png

The interference fringes are exactly what the path imbalance predicts. The free spectral range of an unbalanced MZI is

\[\mathrm{FSR} = \frac{\lambda^2}{n_g \, \Delta L}\]

where \(n_g\) is the group index of the strip waveguide. The mode solver returns it directly when asked with group_index=True, so we can compare:

[14]:
modes = pf.port_modes("TE_1550_500", [pf.C_0 / 1.55], group_index=True)
n_group = modes.data.n_group.item()

fsr_theory = 1.55**2 / (n_group * delta_l) * 1000  # nm

# Measured FSR from the spacing of transmission minima
minima = [
    i
    for i in range(1, len(transmission) - 1)
    if transmission[i] < transmission[i - 1] and transmission[i] < transmission[i + 1]
]
fsr_measured = np.diff(wavelengths[minima]).mean() * 1000  # nm

print(f"Group index n_g = {n_group:.3f}")
print(f"FSR (theory):    {fsr_theory:.2f} nm")
print(f"FSR (simulated): {fsr_measured:.2f} nm")
Starting…
15:30:23 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-3f94da58-2fca-
             41df-b705-a0b6b1ddeed0'.
Progress: 100%
Group index n_g = 4.181
FSR (theory):    7.18 nm
FSR (simulated): 7.16 nm

Theory and simulation agree to well under 1%.

Want to learn more?

  • Continue with Quick Start III to make this MZI active: add a thermal phase shifter to bias it, add electro-optic phase modulators, run a time-domain simulation, and export the project to the PhotonForge GUI.

  • Browse the example library for complete device and system designs.

  • The short guides cover each PhotonForge concept in depth.