Foundry-Compatible PIC Design Flow

PIC layout

In this demo, we’ll learn how to:

  • Load PDK components and custom components from GDSII files;

  • Run component-level simulations (MODE, FDTD);

  • Create a netlist-driven layout of components; and

  • Simulate the complete photonics circuit.

We begin by loading PhotonForge, Tidy3D, the SiEPIC OpenEBL PDK, and other common Python modules we will be using.

[1]:
import matplotlib.pyplot as plt
import numpy as np
import photonforge as pf
import siepic_forge as siepic
import tidy3d as td

# Start the live viewer for interactive visualization
from photonforge.live_viewer import LiveViewer

viewer = LiveViewer()
LiveViewer started at http://localhost:41431

We use the default SiEPIC PDK as our default technology and set some configurations.

[2]:
tech = siepic.ebeam()
pf.config.default_technology = tech

# We lower the default mesh refinement to decrease the sizes of the simulations.
# For more accurate results, higher values should be used.
pf.config.default_mesh_refinement = 12

pf.config.svg_labels = False
td.config.logging_level = "ERROR"

# Set frequency range
wavelengths = np.linspace(1.5, 1.6, 101)

Review the available layers in the PDK:

[3]:
tech.layers
[3]:
NameLayerDescriptionColorPattern
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||

Inspect available PDK components:

[4]:
siepic.component_names
[4]:
{'GC_TE_1310_8degOxide_BB',
 'GC_TE_1550_8degOxide_BB',
 'GC_TM_1310_8degOxide_BB',
 'GC_TM_1550_8degOxide_BB',
 'ebeam_adiabatic_te1550',
 'ebeam_adiabatic_tm1550',
 'ebeam_bdc_te1550',
 'ebeam_crossing4',
 'ebeam_gc_te1550',
 'ebeam_gc_tm1550',
 'ebeam_routing_taper_te1550_w=500nm_to_w=3000nm_L=20um',
 'ebeam_routing_taper_te1550_w=500nm_to_w=3000nm_L=40um',
 'ebeam_splitter_swg_assist_te1310',
 'ebeam_splitter_swg_assist_te1550',
 'ebeam_terminator_te1310',
 'ebeam_terminator_te1550',
 'ebeam_terminator_tm1550',
 'ebeam_y_1310',
 'ebeam_y_1550',
 'ebeam_y_adiabatic',
 'ebeam_y_adiabatic_500pin',
 'taper_si_simm_1310',
 'taper_si_simm_1550'}

Loading a PDK component

We will use a couple of components readily available in the PDK.

[5]:
gc = siepic.component("ebeam_gc_te1550")
viewer(gc)
[5]:
../_images/examples_Foundry_PIC_Design_9_0.svg

We can plot the geometry cross-section with the tidy3d_plot function.

[6]:
_, ax = plt.subplots(1, 1, figsize=(12, 4))
_ = pf.tidy3d_plot(gc, plot_type="structures", y=0, ax=ax)
../_images/examples_Foundry_PIC_Design_11_0.png

Most PDK components will have appropriate ports or terminals for connections, and some will include pre-defined models for S parameter computation.

[7]:
y_splitter = siepic.component("ebeam_y_1550")
viewer(y_splitter)
[7]:
../_images/examples_Foundry_PIC_Design_13_0.svg
[8]:
y_splitter.ports
[8]:
{'P0': Port(center=(-7.4, 0), input_direction=0, spec=PortSpec(description="Strip TE 1550 nm, w=500 nm", width=1.5, limits=(-0.6, 0.82), num_modes=1, added_solver_modes=0, polarization="", target_neff=3.5, default_radius=0, path_profiles=[(0.5, 0, (1, 0))]), extended=True, inverted=False, bend_radius=0),
 'P1': Port(center=(7.4, -2.75), input_direction=180, spec=PortSpec(description="Strip TE 1550 nm, w=500 nm", width=1.5, limits=(-0.6, 0.82), num_modes=1, added_solver_modes=0, polarization="", target_neff=3.5, default_radius=0, path_profiles=[(0.5, 0, (1, 0))]), extended=True, inverted=False, bend_radius=0),
 'P2': Port(center=(7.4, 2.75), input_direction=180, spec=PortSpec(description="Strip TE 1550 nm, w=500 nm", width=1.5, limits=(-0.6, 0.82), num_modes=1, added_solver_modes=0, polarization="", target_neff=3.5, default_radius=0, path_profiles=[(0.5, 0, (1, 0))]), extended=True, inverted=False, bend_radius=0)}
[9]:
y_splitter.models
[9]:
{'Tidy3D': 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=[('P1', 'P2', {'P0': 'P0', 'P2': 'P1'})], bounds=((None, None, None), (None, None, None)), source_gap=None, simulation_updates=None, verbose=True)}

Running FDTD simulations

The Y splitter already has a Tidy3D model and waveguide ports, so a simple call to the s_matrix method is all that is needed to compute its S parameters.

[10]:
s_matrix_tidy3d = y_splitter.s_matrix(frequencies=td.C_0 / wavelengths)
Starting…
07:34:18 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=fdve-62ca33e3-9d0
             0-4d80-95ee-5f029187477b'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=fdve-2de14513-82b
             1-4254-afba-f3d1b905fc26'.
Progress: 100%
[11]:
# Plot the S-parameters
_ = pf.plot_s_matrix(s_matrix_tidy3d, input_ports=["P0"], y="dB")
../_images/examples_Foundry_PIC_Design_18_0.png

Running MODE simulations

It is really easy to inspect port modes as well. The port_modes function will compute the modes and return a Tidy3D ModeSolver object with the mode data.

[12]:
mode_solver = pf.port_modes(
    y_splitter.ports["P0"],
    frequencies=[pf.C_0 / 1.5, pf.C_0 / 1.55, pf.C_0 / 1.6],
    mesh_refinement=40,
    group_index=True,
)
Starting…
07:34:19 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-df493cc8-232f-
             4d2e-a3f0-089329aa6aad'.
Progress: 100%
[13]:
mode_solver.data.to_dataframe()
[13]:
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.998616e+14 0 1.50 2.498887 0.0 0.0 0.985956 0.776479 0.820888 0.176755 4.178812 530.239230
1.934145e+14 0 1.55 2.442763 0.0 0.0 0.983523 0.763881 0.817728 0.190894 4.186544 509.814853
1.873703e+14 0 1.60 2.386398 0.0 0.0 0.980815 0.751972 0.814850 0.206530 4.193567 431.258386
[14]:
_ = mode_solver.plot_field("E", mode_index=0, f=pf.C_0 / 1.55)
../_images/examples_Foundry_PIC_Design_22_0.png

Loading a GDSII file

Custom components can be loaded from GDSII files. We will load a phase shifter designed previously and available in the file thermo-optic_phase_shifter.gds.

To make sure the file is always available, the next cell creates the file automatically from its binary contents, so you do not need to download a separate file to run this notebook.

[15]:
import bz2
import pathlib

_ = pathlib.Path("thermo-optic_phase_shifter.gds").write_bytes(
    bz2.decompress(
        b"BZh91AY&SY\x91\x99q2\x00\x00/\xff\xff\xff\xff\xeeD$\x06\x80\xf16H@P\xaa\x06\xc4@\x00`\x00H@\x80\x00@\t\x02\x80\x04\x00D\x05\x10\xb0\x00\xf6"
        b"5\r\x12\x99\x06\x9a\x00\xd0\xd0\x00\x00\x07\xa8\x00\xd3\xc4\xd2\x06\x88\xa6\x9aa=@\xd3L\x80\x00\xd3CLF\x9a\x00\x00$RiM\xa9\x93FG\xea\x9a"
        b"\x06\x98\x04\x00\x00\x1a`hD\xbd\xe2\t\x93A`K\x99\x820\x10a\xab \x97\x0c\xa9D(\x94\x08\x91$\x11s\x1a\xd5\x14s\x19\xef\x8e\xc7\xd9UYd\x98\x1e"
        b"\xefp7`\x82\\\x87(\xe64\x83\xbc\x13UX\x81\x90\x8a\xa6A\xe9\xcd\x14t\x83\x04be\xecm\x17\xc52\x9e\xc5\xfa\xf8L,\xc6\xcc\x14\x01Q\xa3z\xdd\x04"
        b'\xca\xd3B\x87<*H\x89\x04j"\x86\x10\x82JpR\xeb\xae\xc2p\x9d\x004\x18\x97!\x04\x10\x0b\x89\xa5#\x80@\x9e\xc9{\x01\x04\xb4\xbc\x98\x81\x8bT^'
        b"\xd5\xbd\xc53W\x07\x18\x8c\xa6\x8d\x01N\x9d\x94\xd1l5\xedP\x15\xc0\xd8\x1bb#\xcd\xa2q\x82YN\x1bDl@\x8c :\xb5\x9c\xcf\x8f\x19\x8f\xe2\xeeH"
        b"\xa7\n\x12\x123.&@"
    )
)

With the file created, we can load in with the load_layout function. The loaded layout is a dictionary with all GDSII cells as components:

[16]:
components = pf.load_layout("thermo-optic_phase_shifter.gds")
components.keys()
[16]:
dict_keys(['top_cell'])
[17]:
phase_shifter = components["top_cell"]
viewer(phase_shifter)
[17]:
../_images/examples_Foundry_PIC_Design_27_0.svg

The GDSII format has no standard support for terminals, ports or models, so we have to add them after loading the geometry data:

[18]:
phase_shifter.terminals
[18]:
{}
[19]:
phase_shifter.ports
[19]:
{}
[20]:
phase_shifter.models
[20]:
{}

In most cases, we can automatically detect and add ports. If we know which port specification the geometry uses, we can use it to reduce the number of false positive results. In this case, we look for ports matching the “TE_1550_500” specification from the default technology.

[21]:
ports = phase_shifter.detect_ports(["TE_1550_500"])
ports
[21]:
[Port(center=(0, 0), input_direction=0, spec=PortSpec(description="Strip TE 1550 nm, w=500 nm", width=1.5, limits=(-0.6, 0.82), num_modes=1, added_solver_modes=0, polarization="", target_neff=3.5, default_radius=0, path_profiles=[(0.5, 0, (1, 0))]), extended=True, inverted=False, bend_radius=0),
 Port(center=(50, 0), input_direction=180, spec=PortSpec(description="Strip TE 1550 nm, w=500 nm", width=1.5, limits=(-0.6, 0.82), num_modes=1, added_solver_modes=0, polarization="", target_neff=3.5, default_radius=0, path_profiles=[(0.5, 0, (1, 0))]), extended=True, inverted=False, bend_radius=0)]

Both results are correct, so we can go ahead and add them:

[22]:
phase_shifter.add_port(ports)

viewer(phase_shifter)
[22]:
../_images/examples_Foundry_PIC_Design_35_0.svg

Being a straight waveguide, a semi-analytical waveguide model makes sense for this component.

[23]:
phase_shifter.add_model(pf.WaveguideModel(), "Waveguide")
[23]:
'Waveguide'

Terminals for electrical connection are represented by the 2 rectangles in layer (12, 0), “M2_router”. We can easily find them and add the appropriate terminals for routing later.

[24]:
for structure in phase_shifter.structures[12, 0]:
    phase_shifter.add_terminal(pf.Terminal("M2_router", structure))

viewer(phase_shifter)
[24]:
../_images/examples_Foundry_PIC_Design_39_0.svg

We can store the phase-shifter with the terminal, port and model information if we use a PhotonForge PHF file with the write_phf function:

[25]:
pf.write_phf("thermo-optic_phase_shifter.phf", phase_shifter)

Netlist-driven layout

We can now combine these PDK and custom components to form a cirucit for both layout and simulations. This can be done in an imperative way, by creating and connecting component references using the PhotonForge API, or in a declarative way, using a netlist to fully specify the circuit in a single object. The former approach can be seen in the Quantum Chip and LIDAR examples. Here we show the latter.

We set the defaults that will be used for routing and get the bounds information from the components we will use:

[26]:
pf.config.default_kwargs = {
    "radius": 10,
    "euler_fraction": 0.5,
}
[27]:
y_splitter.bounds()
[27]:
(array([-7.5, -3.5]), array([7.45, 3.5 ]))
[28]:
phase_shifter.bounds()
[28]:
(array([-13.,  -5.]), array([63., 15.]))
[29]:
def create_mzi(length=150, ps_y=25):
    netlist = {
        "name": "MZI",
        # Individual component instances
        "instances": {
            "yin": y_splitter,
            "yout": {"component": y_splitter, "origin": (length, 0), "rotation": 180},
            "ps": {"component": phase_shifter, "origin": (length / 2 - 25, ps_y)},
        },
        # Auto-routing
        "routes": [
            (("yin", "P2"), ("ps", "P0"), pf.parametric.route_s_bend),
            (("ps", "P1"), ("yout", "P1"), pf.parametric.route_s_bend),
            (("yin", "P1"), ("yout", "P2")),
        ],
        # External ports and terminals
        "ports": [("yin", "P0"), ("yout", "P0")],
        "terminals": [("ps", "T0"), ("ps", "T1")],
        # Model for the full circuit
        "models": [pf.CircuitModel()],
    }

    component = pf.component_from_netlist(netlist)
    return component


mzi = create_mzi()
viewer(mzi)
[29]:
../_images/examples_Foundry_PIC_Design_46_0.svg

Circuit simulations

With the circuit model we added to the MZI, we can run circuit simulations by simply calling the s_matrix function again. PhotonForge will automatically run appropriate simulations for the sub-components to calculate the scattering matrix for the whole circuit.

[30]:
s_matrix_mzi = mzi.s_matrix(pf.C_0 / wavelengths)
Starting…
07:34:21 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-87b07ac7-5acb-
             4dd9-b66e-859bbc1c867f'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-ae02e319-d6e3-
             4bfd-a91f-eccb39e57643'.
07:34:22 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-815abae1-23a6-
             45b2-940a-16003ec40b80'.
Progress: 100%
[31]:
_ = pf.plot_s_matrix(s_matrix_mzi, input_ports=["P0"])
../_images/examples_Foundry_PIC_Design_49_0.png

Adding GCs for layout

Just as before, we use a netlist to describe the circuit with the grating couplers from the PDK. This function will use the MZI as argument so that we can vary its parameters later when creating the full chip.

[32]:
def mzi_with_gratings(mzi, fiber_spacing=127, spacing_y=50):
    netlist = {
        "name": "MZI_with_GC",
        "instances": {
            "mzi": {"component": mzi, "origin": (0, spacing_y), "rotation": 90},
            "gc1": {"component": gc, "origin": (0, 0), "rotation": 90},
            "gc2": {"component": gc, "origin": (fiber_spacing, 0), "rotation": 90},
        },
        "routes": [
            (("gc1", "P0"), ("mzi", "P0")),
            (("mzi", "P1"), ("gc2", "P0"), {"radius": fiber_spacing / 2.0}),
        ],
        "ports": [("gc1", "P1"), ("gc2", "P1")],
        "terminals": [("mzi", "T0"), ("mzi", "T1")],
        "models": [pf.CircuitModel()],
    }

    component = pf.component_from_netlist(netlist)
    return component


mzi_gratings = mzi_with_gratings(mzi)

viewer(mzi_gratings)
[32]:
../_images/examples_Foundry_PIC_Design_51_0.svg

Instead of simulating the grating coupler in Tidy3D, we will show how to use pre-computed or measured data to avoid the large simulations.

The Touchstone file ebeam_gc_te1550.sp2 contains example data we can load and assign to the grating coupler with a data model, which could have come from previous measurements, for example.

As with the GDSII file we used previously, we will re-create the Touchstone file from its contents so you don’t have to download it separately.

[33]:
_ = pathlib.Path("ebeam_gc_te1550.s2p").write_text("""[Version] 2.0
# Hz S RI
[Number of Ports] 2
[Two-Port Data Order] 12_21
[Number of Frequencies] 31
[Network Data]
1.897421e+14 -0.0352072 0.0326903 -0.226435 -0.447915 0.210738 0.421143 0.00676895 0.0264931
1.899825e+14 -0.0396145 0.00837585 -0.0362153 -0.512323 0.0336372 0.484589 0.00360995 0.0245064
1.902236e+14 -0.0354181 -0.010244 0.169495 -0.496585 -0.161001 0.47345 0.00310874 0.0214094
1.904653e+14 -0.026563 -0.0276462 0.357034 -0.398698 -0.341562 0.383859 0.00534486 0.0196147
1.907077e+14 -0.00635631 -0.0433821 0.493656 -0.230711 -0.47628 0.225361 0.00848205 0.0208979
1.909506e+14 0.0271043 -0.0439097 0.553475 -0.0179504 -0.538816 0.0205268 0.00983997 0.0250636
1.911942e+14 0.0573636 -0.016617 0.523126 0.20503 -0.513759 -0.198027 0.00772578 0.0298957
1.914384e+14 0.0591343 0.0296758 0.404572 0.400752 -0.400509 -0.392852 0.00269386 0.0325817
1.916832e+14 0.0241182 0.0647929 0.214641 0.535193 -0.214451 -0.52857 -0.00274075 0.0316472
1.919286e+14 -0.0260546 0.0625573 -0.0171573 0.582968 0.0150289 -0.578581 -0.0057533 0.0280115
1.921747e+14 -0.0569269 0.0256378 -0.252092 0.532182 0.249155 -0.53042 -0.00514607 0.0244794
1.924213e+14 -0.0517625 -0.0175119 -0.448512 0.38844 0.44637 -0.388954 -0.00231797 0.0238635
1.926687e+14 -0.0230183 -0.04036 -0.570368 0.175072 0.570088 -0.17651 -0.000417023 0.0269198
1.929166e+14 0.00586192 -0.0394758 -0.594637 -0.0712344 0.596004 0.0705136 -0.00209928 0.031656
1.931652e+14 0.0243362 -0.0276428 -0.515563 -0.307241 0.517319 0.308136 -0.00752106 0.034704
1.934145e+14 0.0364941 -0.0125098 -0.345937 -0.490763 0.34663 0.492995 -0.0141874 0.0337846
1.936644e+14 0.0435652 0.00978416 -0.115382 -0.588478 0.114132 0.59075 -0.0187811 0.0293522
1.939149e+14 0.0348038 0.0393164 0.135057 -0.582551 -0.137855 0.583101 -0.0195603 0.0241891
1.941661e+14 0.0019319 0.0592741 0.360515 -0.473915 -0.363013 0.471707 -0.0175394 0.0213398
1.944179e+14 -0.041254 0.0478519 0.520442 -0.282017 -0.520386 0.27783 -0.01565 0.0219132
1.946704e+14 -0.0635397 0.00473916 0.585755 -0.0419279 -0.582075 0.0380054 -0.0165477 0.0243395
1.949236e+14 -0.0456419 -0.0414984 0.545159 0.201759 -0.538441 -0.203066 -0.0206354 0.0255948
1.951774e+14 -0.000953603 -0.0582347 0.407918 0.404146 -0.399799 -0.401147 -0.0258056 0.0235307
1.954319e+14 0.0376615 -0.0388482 0.201359 0.529297 -0.194252 -0.521024 -0.0291419 0.0184768
1.956870e+14 0.0494522 -0.00407037 -0.0353287 0.556626 0.0382115 -0.54334 -0.0291566 0.0128121
1.959428e+14 0.0390625 0.0235674 -0.25862 0.483795 0.254042 -0.467837 -0.0267002 0.00903373
1.961993e+14 0.020525 0.0388267 -0.428387 0.326759 0.414603 -0.312517 -0.0240809 0.00801482
1.964564e+14 -0.00202446 0.0458016 -0.515291 0.116663 0.493245 -0.10946 -0.0232984 0.00859377
1.967142e+14 -0.0303964 0.0403233 -0.506173 -0.106335 0.479893 0.101871 -0.0246625 0.00861082
1.969727e+14 -0.0543518 0.0125664 -0.406116 -0.300777 0.382185 0.282568 -0.0267262 0.0065854
1.972319e+14 -0.0515758 -0.031471 -0.237294 -0.431789 0.223014 0.401693 -0.0275304 0.00276416
[End]""")

With the file created, we can load it as an S matrix object and use it to create the data model.

[34]:
s_matrix_data = pf.SMatrix.load_snp("ebeam_gc_te1550.s2p")
data_model = pf.DataModel(s_matrix_data, interpolation_method="akima")

_ = gc.add_model(data_model, "Data")
[35]:
_ = pf.plot_s_matrix(gc.s_matrix(pf.C_0 / wavelengths), y="dB", input_ports=["P0"])
Progress: 100%
../_images/examples_Foundry_PIC_Design_56_1.png
[36]:
s_matrix_mzi_gratings = mzi_gratings.s_matrix(pf.C_0 / wavelengths)
_ = pf.plot_s_matrix(s_matrix_mzi_gratings, input_ports=["P0"])
Starting…
07:34:23 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-cf0206c8-e3f8-
             43a6-ac7e-5c609f0aee96'.
             Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-166fb7ea-56cf-
             4378-bf1e-ad26074f506c'.
07:34:24 -03 Loading simulation from local cache. View cached task using web UI
             at
             'https://tidy3d.simulation.cloud/workbench?taskId=mo-a1178249-684e-
             43fe-8508-0c742acb7209'.
Progress: 100%
../_images/examples_Foundry_PIC_Design_57_5.png

Electrical connections

For the electrical connections we add bond pads to the final layout.

[37]:
tech.layers
[37]:
NameLayerDescriptionColorPattern
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||
[38]:
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)
[38]:
../_images/examples_Foundry_PIC_Design_60_0.svg
[39]:
def mzi_with_bond_pads(mzi, bp_x=-250, bp_y=500, bp_spacing=150):
    netlist = {
        "name": "Circuit",
        "instances": {
            "mzi": mzi_with_gratings(mzi),
            "bp1": {"component": bp, "origin": (bp_x, bp_y)},
            "bp2": {"component": bp, "origin": (bp_x + bp_spacing, bp_y)},
        },
        "terminal routes": [
            (("mzi", "T0"), ("bp1", "T0"), {"width": 30, "overlap_fraction": 0.5}),
            (("mzi", "T1"), ("bp2", "T0"), {"width": 30, "overlap_fraction": 0.5}),
        ],
        "ports": [("mzi", "P0"), ("mzi", "P1")],
        "terminals": [("bp1", "T0"), ("bp2", "T0")],
        "models": [pf.CircuitModel()],
    }

    component = pf.component_from_netlist(netlist)
    return component


mzi_bp = mzi_with_bond_pads(mzi)
viewer(mzi_bp)
[39]:
../_images/examples_Foundry_PIC_Design_61_0.svg

Final chip layout

We can now easily repeat this design (or variations of it) to build the final layout.

We will vary the MZI parameters to create a few variations:

[40]:
variations = []

for length in [150, 250]:
    for ps_y in [20, 25, 30]:
        # Create the variation
        base_mzi = create_mzi(length=length, ps_y=ps_y)
        variation = mzi_with_bond_pads(base_mzi)

        # Add a label to identify the device (this could be done in the create_mzi
        # or any other component creation function directly too)
        variation_id = f"L: {length}\nY: {ps_y}"
        variation.add("Text", *pf.text(variation_id, size=50, origin=(-200, 20)))
        variations.append(variation)

viewer(variations[0])
[40]:
../_images/examples_Foundry_PIC_Design_63_0.svg
[41]:
_, ax = plt.subplots(1, 1)
for i, variation in enumerate(variations):
    s = variation.s_matrix(pf.C_0 / wavelengths, show_progress=False)
    ax.plot(s.wavelengths, np.abs(s["P0@0", "P1@0"]) ** 2, label=str(i))

ax.set(xlabel="Wavelength (μm)", ylabel="|S₁₀|²")
_ = ax.legend()
../_images/examples_Foundry_PIC_Design_64_0.png
[42]:
main = pf.Component("MZM_PIC")

for i, variation in enumerate(variations):
    var_ref = pf.Reference(variation, origin=(600 * i, 0))
    main.add(var_ref)

# Add device region
chip_bounds = pf.envelope(main, 200, use_box=True)
main.add("DevRec", chip_bounds)

# Export the final geometry to a GDSII file
main.write_gds("MZM_PIC.gds")

viewer(main)
/tmp/ipykernel_957910/2326998191.py:12: RuntimeWarning: The following components have been renamed in the layout because all names must be non-empty and unique: 'Circuit_1', 'MZI_with_GC_1', 'Circuit_2', 'MZI_1', 'Circuit_3', 'MZI_with_GC_2', 'MZI_2', 'MZI_3', 'Circuit_4', 'Circuit_5', 'MZI_with_GC_3', 'MZI_with_GC_4', 'MZI_4', 'MZI_with_GC_5', 'MZI_5'.
  main.write_gds("MZM_PIC.gds")
[42]:
../_images/examples_Foundry_PIC_Design_65_1.svg