Circuit models#

We provide circuit models for the PDK elements, implemented using the sax circuit simulator. A dictionary with the available models can be obtained by running:

models = lnoi400.get_models()

These models are useful for constructing the scattering matrix of a building block, or of a hierarchical circuit. They are obtained by experimental characterization of the building blocks and with FDTD simulations, that provide the full wavelength-dependent behaviour. Since the lnoi400 PDK is conceived for optical C-band operation, the model results should not be trusted below 1500 or above 1600 nm.

Examples of circuit simulation using sax#

import numpy as np
from functools import partial
import matplotlib.pyplot as plt
import gdsfactory as gf
import sax
import gplugins.sax as gs
import lnoi400

Circuit simulation of a splitter tree#

First, we display the circuit model of the 1x2 MMI shipped with the PDK.

splitter = gf.get_component("mmi1x2_optimized1550")
splitter.plot()
../_images/b9546386f5b218b72894ae7691422338443a20288a5aa53a96fa6e22e61286df.png
pcell_models = lnoi400.get_models()
mmi_model = pcell_models["mmi1x2_optimized1550"]
_ = gs.plot_model(
    mmi_model,
    wavelength_start=1.5,
    wavelength_stop=1.6,
    port1="o1",
    ports2=("o2", "o3"),
)
../_images/ec95679bb48deafa0ce1a46b8c4e3a47079126a8041fdc76a5692c078cd9bcb4.png

We then build a simple, two-level-deep splitter tree, creating a new gdsfactory hierarchical component.

@gf.cell
def splitter_chain(
    splitter = gf.get_component("mmi1x2_optimized1550"),
    column_offset = (250.0, 200.0),
    routing_reff = 90.0
) -> gf.Component:

    c = gf.Component()
    s0 = c << splitter
    s01 = c << splitter
    s02 = c << splitter
    s01.dmove(
        s01.ports["o1"].dcenter,
        s0.ports["o2"].dcenter + np.array(column_offset)
    )
    s02.dmove(
        s02.ports["o1"].dcenter,
        s0.ports["o3"].dcenter + np.array([column_offset[0], - column_offset[1]])
    )

    # Bend spec

    routing_bend = gf.get_component('L_turn_bend', radius=routing_reff)

    # Routing between splitters

    for ports_to_route in [
        (s0.ports["o2"], s01.ports["o1"]),
        (s0.ports["o3"], s02.ports["o1"]),
    ]:

        gf.routing.route_single(
            c,
            ports_to_route[0],
            ports_to_route[1],
            start_straight_length=5.0,
            end_straight_length=5.0,
            cross_section="xs_rwg1000",
            bend=routing_bend,
        )

    # Expose the I/O ports

    c.add_port(name="in", port=s0.ports["o1"])
    c.add_port(name="out_00", port=s01.ports["o2"])
    c.add_port(name="out_01", port=s01.ports["o3"])
    c.add_port(name="out_10", port=s02.ports["o2"])
    c.add_port(name="out_11", port=s02.ports["o3"])

    return c

chain = splitter_chain()
chain
../_images/b9051d695b3acf975490e4388f8ddc806af0d0260187f67d894175b98e997971.png

Let’s compile the circuit simulation using sax.

nl = chain.get_netlist()

models = {
    # The Euler bend should be sufficiently low-loss to be approximated with a straight waveguide
    # (if the frequency is not too low)
    "L_turn_bend": pcell_models["straight_rwg1000"],
    "straight": pcell_models["straight_rwg1000"],
    "mmi1x2_optimized1550": pcell_models["mmi1x2_optimized1550"],
}
circuit, _ = sax.circuit(netlist=nl, models=models)

_ = gs.plot_model(
    circuit,
    wavelength_start=1.5,
    wavelength_stop=1.6,
    port1="in",
    ports2=("out_00", "out_11"),
)
../_images/8787837507a8060f790e49bca1c0455cbf913c716de16eb63b22ac70d2f1d81e.png

Simulation of a Mach-Zehnder interferometer with a thermo-optical phase shifter#

First we take a look at the cell layout.

mzm_specs = dict(
    modulation_length=1500.0,
    with_heater=True,
    bias_tuning_section_length=1000.0,
)
mzm = gf.get_component(
    "mzm_unbalanced",
    **mzm_specs,
    )
mzm.plot()
../_images/df213b0e96b045e36f9de4fa3d301828eb028dddfb54c6da2913dec7c36be773.png

Then, we retrieve the circuit model and evaluate it for different wavelengths and voltages.

mzm_specs = dict(
    modulation_length=1500.0,
    heater_length=1000.0,
)
mzm_model = partial(
    pcell_models["mzm_unbalanced"],
    **mzm_specs,
)

fig = plt.figure(figsize=(7.5, 5))

wls = [1.4, 1.5, 1.6]
V_scan = np.linspace(-3, 3, 99)
for wl in wls:
    P_out = [np.abs(mzm_model(
        wl=wl,
        V_ht=V,
        )["o2", "o1"])
        for V in V_scan]
    plt.semilogy(V_scan, P_out, label=f'{wl} um')

plt.legend(loc='best')
plt.xlabel("Voltage (V)")
_ = plt.ylabel("MZM transmission")
../_images/6fc6f4188b46b7c5a5c3af0d225485e7ab58d240037803a92e0b6a246993b2b2.png