Layout-Aware Monte Carlo Simulations for Yield Estimation#

Manufacturing variability can cause fabrication errors in waveguide width and thickness, which can affect the device performance. Hence, incorporating manufacturing variability into the photonic device design process is crucial. Simphony provides the ability to run layout-aware Monte Carlo simulations for yield estimation to aid in robust photonic device design. This tutorial will walk you through the codes found in examples/layout_aware.py of the Simphony repository. We expect you to have read the previous tutorial, Mach-Zehnder interferometer.

The Workflow#

The workflow for running layout-aware Monte Carlo simulations is as follows:

  1. instantiate the components

  2. generate a layout using the components’ component attributes

  3. connect the components to form a circuit

  4. run the simulation

  5. extract and plot the results

Example - Mach-Zehnder Interferometer (MZI)#

We will run a layout-aware Monte Carlo simulation for the Mach-Zehnder Interferometer (MZI) described in the previous example.

First we need to import the necessary Simphony modules. We will need the siepic model library, the Simulation, Laser, and Detector. We will also need to import gdsfactory to generate the layout. We will also import matplotlib.pyplot, from outside of Simphony, to view the results of our simulation.

import gdsfactory as gf
import matplotlib.pyplot as plt
import numpy as np

from simphony.libraries import siepic
from simphony.simulation import Detector, Laser, Simulation

We then create all the components and give them names. These include the grating couplers, the Y-branches, and the waveguides (which can be defined at any arbitrary length, on the condition that the two lengths are different).

gc_input = siepic.GratingCoupler(name="gcinput")
y_splitter = siepic.YBranch(name="ysplit")
wg_long = siepic.Waveguide(length=150e-6, name="wglong")
wg_short = siepic.Waveguide(length=50e-6, name="wgshort")
y_recombiner = siepic.YBranch(name="y_recombiner")
gc_output = siepic.GratingCoupler(name="gcoutput")

We then use the components’ component attributes to create the layout. The component attributes are gdsfactory.Component objects, and so can be used to create a layout. We will next define a Parametric Cell (PCell) for the MZI. We will connect the components to form a circuit, route the Waveguides using gdsfactory's routing functions.

@gf.cell
def mzi():
    c = gf.Component("mzi")

    ysplit = c << y_splitter.component

    gcin = c << gc_input.component

    gcout = c << gc_output.component

    yrecomb = c << y_recombiner.component

    yrecomb.move(destination=(0, -55.5))
    gcout.move(destination=(-20.4, -55.5))
    gcin.move(destination=(-20.4, 0))

    gc_input["pin1"].connect(y_splitter, gcin, ysplit)
    gc_output["pin1"].connect(y_recombiner["pin1"], gcout, yrecomb)
    y_splitter["pin2"].connect(wg_long)
    y_recombiner["pin3"].connect(wg_long)
    y_splitter["pin3"].connect(wg_short)
    y_recombiner["pin2"].connect(wg_short)

    wg_long_ref = gf.routing.get_route_from_steps(
        ysplit.ports["pin2"],
        yrecomb.ports["pin3"],
        steps=[{"dx": 91.75 / 2}, {"dy": -61}],
    )
    wg_short_ref = gf.routing.get_route_from_steps(
        ysplit.ports["pin3"],
        yrecomb.ports["pin2"],
        steps=[{"dx": 47.25 / 2}, {"dy": -50}],
    )

    wg_long.path = wg_long_ref
    wg_short.path = wg_short_ref

    c.add(wg_long_ref.references)
    c.add(wg_short_ref.references)

    c.add_port("o1", port=gcin.ports["pin2"])
    c.add_port("o2", port=gcout.ports["pin2"])

    return c

We can then call the function, and visualize the layout in KLayout.

c = mzi()
c.show()
layout

We can also view a 3D representation of the layout.

c.to_3d().show("gl")

Layout-Aware Monte Carlo Simulation#

We use the Simulation class to run a simulation. We attach a Laser to one of the GratingCouplers, and a Detector to the other GratingCoupler.

with Simulation() as sim:
    l = Laser(power=1)
    l.freqsweep(187370000000000.0, 199862000000000.0)
    l.connect(gc_input["pin2"])
    d = Detector()
    d.connect(gc_output["pin2"])

    results = sim.layout_aware_simulation(c)

Here, we can pass in the standard deviations of the widths and thicknesses, as well as the correlation length as arguments to the layout_aware_simulation method. For this example, we use the default values.

After the simulation is run, we can extract the results, and plot them. We will see several, slightly different curves due to random variations incorporated into the components’ widths and thicknesses.

f = l.freqs
for run in results:
    p = []
    for sample in run:
        for data_list in sample:
            for data in data_list:
                p.append(data)
    plt.plot(f, p)

run_0 = results[0]
p = []
for sample in run_0:
    for data_list in sample:
        for data in data_list:
            p.append(data)
plt.plot(f, p, "k")
plt.title("MZI Layout Aware Monte Carlo")
plt.show()

You should see something similar to this graph when you run your MZI now:

layout-aware simulation

From our data, we can then compute various performance markers which are sensitive to width and thickness variations.