Skip to content

Circuit

SAX Circuits

import gdsfactory as gf

import sax
from sax.circuits import (
    _create_dag,
    _find_leaves,
    _find_root,
    _flat_circuit,
    _validate_models,
    draw_dag,
)

Let's start by creating a simple recursive netlist with gdsfactory.

NOTE: We are using gdsfactory to create our netlist because it allows us to see the circuit we want to simulate and because we're striving to have a compatible netlist implementation in SAX.

However... gdsfactory is not a dependency of SAX. You can also define your circuits by hand (see SAX Quick Start Notebook) or you can use another tool to programmatically construct your netlists.

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

    # components
    mmi_in = gf.components.mmi1x2()
    mmi_out = gf.components.mmi2x2()
    bend = gf.components.bend_euler()
    half_delay_straight = gf.components.straight(length=delta_length / 2.0)

    # references
    mmi_in = c.add_ref(mmi_in, name="mmi_in")
    mmi_out = c.add_ref(mmi_out, name="mmi_out")
    straight_top1 = c.add_ref(half_delay_straight, name="straight_top1")
    straight_top2 = c.add_ref(half_delay_straight, name="straight_top2")
    bend_top1 = c.add_ref(bend, name="bend_top1")
    bend_top2 = c.add_ref(bend, name="bend_top2").dmirror()
    bend_top3 = c.add_ref(bend, name="bend_top3").dmirror()
    bend_top4 = c.add_ref(bend, name="bend_top4")
    bend_btm1 = c.add_ref(bend, name="bend_btm1").dmirror()
    bend_btm2 = c.add_ref(bend, name="bend_btm2")
    bend_btm3 = c.add_ref(bend, name="bend_btm3")
    bend_btm4 = c.add_ref(bend, name="bend_btm4").dmirror()

    # connections
    bend_top1.connect("o1", mmi_in.ports["o2"])
    straight_top1.connect("o1", bend_top1.ports["o2"])
    bend_top2.connect("o1", straight_top1.ports["o2"])
    bend_top3.connect("o1", bend_top2.ports["o2"])
    straight_top2.connect("o1", bend_top3.ports["o2"])
    bend_top4.connect("o1", straight_top2.ports["o2"])

    bend_btm1.connect("o1", mmi_in.ports["o3"])
    bend_btm2.connect("o1", bend_btm1.ports["o2"])
    bend_btm3.connect("o1", bend_btm2.ports["o2"])
    bend_btm4.connect("o1", bend_btm3.ports["o2"])

    mmi_out.connect("o1", bend_btm4.ports["o2"])

    # ports
    c.add_port(
        "o1",
        port=mmi_in.ports["o1"],
    )
    c.add_port("o2", port=mmi_out.ports["o3"])
    c.add_port("o3", port=mmi_out.ports["o4"])
    return c


@gf.cell
def twomzi():
    c = gf.Component()

    # instances
    mzi1 = mzi(delta_length=10)
    mzi2 = mzi(delta_length=20)

    # references
    mzi1_ = c.add_ref(mzi1, name="mzi1")
    mzi2_ = c.add_ref(mzi2, name="mzi2")

    # connections
    mzi2_.connect("o1", mzi1_.ports["o2"])

    # ports
    c.add_port("o1", port=mzi1_.ports["o1"])
    c.add_port("o2", port=mzi2_.ports["o2"])
    return c
comp = twomzi()
comp

png

recnet = sax.netlist(comp.get_netlist(recursive=True))
mzi1_comp = recnet["twomzi"]["instances"]["mzi1"]["component"]
flatnet = recnet[mzi1_comp]

To be able to model this device we'll need some SAX dummy models:

def bend_euler(
    angle=90.0,
    p=0.5,
    # cross_section="strip",
    # direction="ccw",
    # with_bbox=True,
    # with_arc_floorplan=True,
    # npoints=720,
):
    return sax.reciprocal({("o1", "o2"): 1.0})
def mmi1x2(
    width=0.5,
    width_taper=1.0,
    length_taper=10.0,
    length_mmi=5.5,
    width_mmi=2.5,
    gap_mmi=0.25,
    # cross_section= strip,
    # taper= {function= taper},
    # with_bbox= True,
):
    return sax.reciprocal(
        {
            ("o1", "o2"): 0.45**0.5,
            ("o1", "o3"): 0.45**0.5,
        }
    )
def mmi2x2(
    width=0.5,
    width_taper=1.0,
    length_taper=10.0,
    length_mmi=5.5,
    width_mmi=2.5,
    gap_mmi=0.25,
    # cross_section= strip,
    # taper= {function= taper},
    # with_bbox= True,
):
    return sax.reciprocal(
        {
            ("o1", "o3"): 0.45**0.5,
            ("o1", "o4"): 1j * 0.45**0.5,
            ("o2", "o3"): 1j * 0.45**0.5,
            ("o2", "o4"): 0.45**0.5,
        }
    )
def straight(
    length=0.01,
    # npoints=2,
    # with_bbox=True,
    # cross_section=...
):
    return sax.reciprocal({("o1", "o2"): 1.0})

In SAX, we usually aggregate the available models in a models dictionary:

models = {
    "straight": straight,
    "bend_euler": bend_euler,
    "mmi1x2": mmi1x2,
    "mmi2x2": mmi2x2,
}

We can also create some dummy multimode models:

def bend_euler_mm(
    angle=90.0,
    p=0.5,
    # cross_section="strip",
    # direction="ccw",
    # with_bbox=True,
    # with_arc_floorplan=True,
    # npoints=720,
):
    return sax.reciprocal(
        {
            ("o1@TE", "o2@TE"): 0.9**0.5,
            # ('o1@TE', 'o2@TM'): 0.01**0.5,
            # ('o1@TM', 'o2@TE'): 0.01**0.5,
            ("o1@TM", "o2@TM"): 0.8**0.5,
        }
    )
def mmi1x2_mm(
    width=0.5,
    width_taper=1.0,
    length_taper=10.0,
    length_mmi=5.5,
    width_mmi=2.5,
    gap_mmi=0.25,
    # cross_section= strip,
    # taper= {function= taper},
    # with_bbox= True,
):
    return sax.reciprocal(
        {
            ("o1@TE", "o2@TE"): 0.45**0.5,
            ("o1@TE", "o3@TE"): 0.45**0.5,
            ("o1@TM", "o2@TM"): 0.41**0.5,
            ("o1@TM", "o3@TM"): 0.41**0.5,
            ("o1@TE", "o2@TM"): 0.01**0.5,
            ("o1@TM", "o2@TE"): 0.01**0.5,
            ("o1@TE", "o3@TM"): 0.02**0.5,
            ("o1@TM", "o3@TE"): 0.02**0.5,
        }
    )
def mmi2x2_mm(
    width=0.5,
    width_taper=1.0,
    length_taper=10.0,
    length_mmi=5.5,
    width_mmi=2.5,
    gap_mmi=0.25,
    # cross_section= strip,
    # taper= {function= taper},
    # with_bbox= True,
):
    return sax.reciprocal(
        {
            ("o1@TE", "o3@TE"): 0.45**0.5,
            ("o1@TE", "o4@TE"): 1j * 0.45**0.5,
            ("o2@TE", "o3@TE"): 1j * 0.45**0.5,
            ("o2@TE", "o4@TE"): 0.45**0.5,
            ("o1@TM", "o3@TM"): 0.45**0.5,
            ("o1@TM", "o4@TM"): 1j * 0.45**0.5,
            ("o2@TM", "o3@TM"): 1j * 0.45**0.5,
            ("o2@TM", "o4@TM"): 0.45**0.5,
        }
    )
def straight_mm(
    length=0.01,
    # npoints=2,
    # with_bbox=True,
    # cross_section=...
):
    return sax.reciprocal(
        {
            ("o1@TE", "o2@TE"): 1.0,
            ("o1@TM", "o2@TM"): 1.0,
        }
    )
models_mm = {
    "straight": straight_mm,
    "bend_euler": bend_euler_mm,
    "mmi1x2": mmi1x2_mm,
    "mmi2x2": mmi2x2_mm,
}

We can now represent our recursive netlist model as a Directed Acyclic Graph:

dag = _create_dag(recnet, models)
draw_dag(dag)

png

Note that the DAG depends on the models we supply. We could for example stub one of the sub-netlists by a pre-defined model:

dag_ = _create_dag(recnet, {**models, "mzi_DL10": mmi2x2})
draw_dag(dag_, with_labels=True)

png

This is useful if we for example pre-calculated a certain model.

We can easily find the root of the DAG:

_find_root(dag)
['twomzi']

Similarly we can find the leaves:

_find_leaves(dag)
['bend_euler', 'mmi1x2', 'mmi2x2', 'straight']

To be able to simulate the circuit, we need to supply a model for each of the leaves in the dependency DAG. Let's write a validator that checks this

models
{'straight': <function __main__.straight(length=0.01)>,
 'bend_euler': <function __main__.bend_euler(angle=90.0, p=0.5)>,
 'mmi1x2': <function __main__.mmi1x2(width=0.5, width_taper=1.0, length_taper=10.0, length_mmi=5.5, width_mmi=2.5, gap_mmi=0.25)>,
 'mmi2x2': <function __main__.mmi2x2(width=0.5, width_taper=1.0, length_taper=10.0, length_mmi=5.5, width_mmi=2.5, gap_mmi=0.25)>}
models = _validate_models(models, dag)

We can now dow a bottom-up simulation. Since at the bottom of the DAG, our circuit is always flat (i.e. not hierarchical) we can implement a minimal _flat_circuit definition, which only needs to work on a flat (non-hierarchical circuit):

from sax.netlists import convert_nets_to_connections

flatnet = convert_nets_to_connections(recnet[mzi1_comp])

single_mzi = _flat_circuit(
    flatnet["instances"],
    flatnet["connections"],
    flatnet["ports"],
    models,
    "klu",
)
single_mzi()
(Array([[0.  +0.j  , 0.45+0.45j, 0.45+0.45j],
        [0.45+0.45j, 0.  +0.j  , 0.  +0.j  ],
        [0.45+0.45j, 0.  +0.j  , 0.  +0.j  ]], dtype=complex128),
 {'o1': 0, 'o2': 1, 'o3': 2})

The resulting circuit is just another SAX model (i.e. a python function) returing an SType:

?single_mzi

Let's 'execute' the circuit:

Note that we can also supply multimode models:

flatnet = convert_nets_to_connections(recnet[mzi1_comp])
single_mzi = _flat_circuit(
    flatnet["instances"],
    flatnet["connections"],
    flatnet["ports"],
    models_mm,
    "klu",
)
single_mzi()
(Array([[0.        +0.j        , 0.        +0.j        ,
         0.3645    +0.3645j    , 0.06071573+0.04293251j,
         0.3645    +0.3645j    , 0.04293251+0.06071573j],
        [0.        +0.j        , 0.        +0.j        ,
         0.07684335+0.05433645j, 0.27490216+0.27490216j,
         0.05433645+0.07684335j, 0.27490216+0.27490216j],
        [0.3645    +0.3645j    , 0.07684335+0.05433645j,
         0.        +0.j        , 0.        +0.j        ,
         0.        +0.j        , 0.        +0.j        ],
        [0.06071573+0.04293251j, 0.27490216+0.27490216j,
         0.        +0.j        , 0.        +0.j        ,
         0.        +0.j        , 0.        +0.j        ],
        [0.3645    +0.3645j    , 0.05433645+0.07684335j,
         0.        +0.j        , 0.        +0.j        ,
         0.        +0.j        , 0.        +0.j        ],
        [0.04293251+0.06071573j, 0.27490216+0.27490216j,
         0.        +0.j        , 0.        +0.j        ,
         0.        +0.j        , 0.        +0.j        ]],      dtype=complex128),
 {'o1@TE': 0, 'o1@TM': 1, 'o2@TE': 2, 'o2@TM': 3, 'o3@TE': 4, 'o3@TM': 5})

Now that we can handle flat circuits the extension to hierarchical circuits is not so difficult:

single mode simulation:

double_mzi, info = sax.circuit(netlist=recnet, models=models, backend="default")
double_mzi()
{('o1', 'o1'): Array(0.+0.j, dtype=complex128),
 ('o1', 'o2'): Array(0.+0.405j, dtype=complex128),
 ('o2', 'o1'): Array(0.+0.405j, dtype=complex128),
 ('o2', 'o2'): Array(0.+0.j, dtype=complex128)}

multi mode simulation:

double_mzi, info = sax.circuit(
    netlist=recnet, models=models_mm, backend="default", return_type="sdict"
)
double_mzi()
{('o1@TE', 'o1@TE'): Array(0.+0.j, dtype=complex128),
 ('o1@TE', 'o1@TM'): Array(0.+0.j, dtype=complex128),
 ('o1@TE', 'o2@TE'): Array(0.0023328+0.27231865j, dtype=complex128),
 ('o1@TE', 'o2@TM'): Array(0.01137063+0.06627291j, dtype=complex128),
 ('o1@TM', 'o1@TE'): Array(0.+0.j, dtype=complex128),
 ('o1@TM', 'o1@TM'): Array(0.+0.j, dtype=complex128),
 ('o1@TM', 'o2@TE'): Array(0.01439096+0.08387665j, dtype=complex128),
 ('o1@TM', 'o2@TM'): Array(0.0023328+0.15774055j, dtype=complex128),
 ('o2@TE', 'o1@TE'): Array(0.0023328+0.27231865j, dtype=complex128),
 ('o2@TE', 'o1@TM'): Array(0.01439096+0.08387665j, dtype=complex128),
 ('o2@TE', 'o2@TE'): Array(0.+0.j, dtype=complex128),
 ('o2@TE', 'o2@TM'): Array(0.+0.j, dtype=complex128),
 ('o2@TM', 'o1@TE'): Array(0.01137063+0.06627291j, dtype=complex128),
 ('o2@TM', 'o1@TM'): Array(0.0023328+0.15774055j, dtype=complex128),
 ('o2@TM', 'o2@TE'): Array(0.+0.j, dtype=complex128),
 ('o2@TM', 'o2@TM'): Array(0.+0.j, dtype=complex128)}

sometimes it's useful to get the required circuit model names to be able to create the circuit:

sax.get_required_circuit_models(recnet, models)
['mmi1x2', 'mmi2x2']