Microring Modulator

In this notebook, we show the layout for a 5×200 Gbps microring modulator chip derived from the works by Yuan et al. [1].

References

  1. Yuan, Y. et al. “A 5 × 200 Gbps microring modulator silicon chip empowered by two-segment Z-shape junctions.” Nat Commun 15, 918 (2024), doi: 10.1038/s41467-024-45301-3.

[1]:
import numpy as np
import photonforge as pf
import siepic_forge as siepic
from photonforge.live_viewer import LiveViewer

viewer = LiveViewer()
Starting live viewer at http://localhost:5001

Choose default technology and keyword arguments for parametric components.

[2]:
tech = siepic.ebeam()

pf.config.default_technology = tech
pf.config.default_kwargs["radius"] = 40
pf.config.default_kwargs["euler_fracton"] = 0.5
pf.config.default_kwargs["port_spec"] = tech.ports["TE_1550_500"]

pf.config.svg_labels = False

We crate the ring as a parametric component with a few adjustable parameters:

[3]:
@pf.parametric_component(name_prefix="MRM")
def micro_ring_modulator(
    *,
    taper_length=20,
    coupling_gap=0.6,
    coupling_length=5,
    ring_radius=20,
    euler_fraction=0.15,
    s_bend_offset=5,
    slab_width=3,
    metal_gap=3.5,
    metal_width=3,
    sector_angles=[35, 45, 220, 320],
    heater_width=1.5,
    heater_pad_distance=30,
    electrode_length=50,
    draw_electrodes=True
):
    c = pf.Component()

    port_spec = pf.config.default_kwargs["port_spec"]
    wg_width, _ = port_spec.path_profile_for("Si")
    s_bend_length = 5 * s_bend_offset

    # ring function that will be re-used internally
    def ring_path(width):
        return (
            pf.Path((-coupling_length / 2, -ring_radius), width)
            .segment((coupling_length / 2, -ring_radius))
            .turn(
                180,
                ring_radius,
                euler_fraction,
                endpoint=(coupling_length / 2, ring_radius),
            )
            .segment((-coupling_length / 2, ring_radius))
            .turn(
                180,
                ring_radius,
                euler_fraction,
                endpoint=(-coupling_length / 2, -ring_radius),
            )
        )

    # ring resonator
    ring = ring_path(wg_width)

    x_port = -coupling_length / 2 - s_bend_length - taper_length
    y_port = -ring_radius - wg_width - coupling_gap - s_bend_offset
    # Bus waveguide
    bus = (
        pf.Path((x_port, y_port), wg_width)
        .segment((taper_length, 0), relative=True)
        .s_bend((s_bend_length, s_bend_offset), euler_fraction, relative=True)
        .segment((coupling_length, 0), relative=True)
        .s_bend((s_bend_length, -s_bend_offset), euler_fraction, relative=True)
        .segment((-x_port, y_port))
    )

    # Si-slab
    slab = [
        # bus path with taper
        pf.Path((x_port, y_port), wg_width)
        .segment((taper_length, 0), slab_width * 2, relative=True)
        .segment((s_bend_length * 2 + coupling_length, 0), relative=True)
        .segment((-x_port, y_port), wg_width),
        # middle rectangle
        pf.Rectangle(
            (-coupling_length / 2 - s_bend_length, y_port - slab_width),
            (coupling_length / 2 + s_bend_length, 0),
        ),
        # slab around the ring
        pf.envelope(ring, offset=slab_width * 2),
    ]
    # combine into a single polygon
    slab = pf.boolean(slab, [], "+")

    c.add("Si", bus, ring, "Si slab", *slab)

    c.add_port(
        [
            pf.Port((x_port, y_port), 0, port_spec),
            pf.Port((-x_port, y_port), 180, port_spec),
        ]
    )

    if draw_electrodes:
        # Metal electrodes for LSB and MSB
        electrode_ring = ring_path(metal_width * 2 + metal_gap)
        electrode_gap = ring_path(metal_gap)

        sector1 = pf.Circle(
            radius=(
                electrode_ring.x_max + 2 * metal_width,
                electrode_ring.y_max + 2 * metal_width,
            ),
            sector=sector_angles[:2],
        )
        sector2 = sector1.copy()
        sector2.sector = sector_angles[2:]

        x_term_out = -coupling_length / 2 - ring_radius - electrode_length
        x_term_in = -coupling_length / 2 - ring_radius / 2
        term_width = min(ring_radius, 4 * metal_width)
        term_size = (term_width, term_width)

        msb_taper = (
            pf.Path((x_term_out, 0), term_width)
            .segment((electrode_length, 0), ring_radius, relative=True)
            .segment((x_term_in, 0), term_width)
        )
        lsb_taper = (
            pf.Path((-x_term_out, 0), term_width)
            .segment((-electrode_length, 0), ring_radius, relative=True)
            .segment((-x_term_in, 0), term_width)
        )

        electrodes = pf.boolean(
            [electrode_ring, msb_taper, lsb_taper],
            [electrode_gap, sector1, sector2],
            "-",
        )

        c.add("M2_router", *electrodes)

        c.add_terminal(
            {
                "MSB+": pf.Terminal(
                    "M2_router", pf.Rectangle(center=(x_term_out, 0), size=term_size)
                ),
                "MSB-": pf.Terminal(
                    "M2_router", pf.Rectangle(center=(x_term_in, 0), size=term_size)
                ),
                "LSB+": pf.Terminal(
                    "M2_router", pf.Rectangle(center=(-x_term_out, 0), size=term_size)
                ),
                "LSB-": pf.Terminal(
                    "M2_router", pf.Rectangle(center=(-x_term_in, 0), size=term_size)
                ),
            }
        )

        x_term = -heater_width - term_width / 2
        y_term = -ring_radius - heater_pad_distance

        heater_ring = ring_path(heater_width)
        heater_taper = pf.Path((0, y_term), 2 * (heater_width + term_width)).segment(
            (0, -ring_radius), 4 * heater_width
        )
        heater_gap = pf.Path((0, y_term - heater_width), 2 * heater_width).segment(
            (0, 0)
        )

        heater = pf.boolean([heater_ring, heater_taper], [heater_gap], "-")

        c.add_terminal(
            {
                "H+": pf.Terminal(
                    "M1_heater",
                    pf.Rectangle(center=(x_term, y_term), size=(term_width, 0)),
                ),
                "H-": pf.Terminal(
                    "M1_heater",
                    pf.Rectangle(center=(-x_term, y_term), size=(term_width, 0)),
                ),
            }
        )

        c.add("M1_heater", *heater)

    return c


mrm = micro_ring_modulator(draw_electrodes=True)
viewer(mrm)
[3]:
../_images/examples_Microring_Modulator_5_0.svg

The bond pads can be used directly from the PDK:

[4]:
bp = siepic.component("ebeam_BondPad")
bp
[4]:
../_images/examples_Microring_Modulator_7_0.svg

Now we combine a single ring with the bond pads:

[5]:
@pf.parametric_component(name_prefix="MRM_BP")
def mrm_with_bond_pads(*, spacing=150, driver_offset=350, heater_offset=200):
    c = pf.Component()

    bp_driver = pf.Reference(
        bp,
        (-2 * spacing, driver_offset),
        columns=6,
        spacing=(spacing, 0),
    )

    bp_heater = pf.Reference(
        bp,
        (-0.5 * spacing, -heater_offset),
        columns=3,
        spacing=(spacing, 0),
    )

    c.add(bp_driver, bp_heater)

    mrm_ref = c.add_reference(mrm)
    c.add_port([mrm_ref["P0"], mrm_ref["P1"]])

    c.add(
        pf.parametric.route_taper(
            terminal1=(mrm_ref, "MSB+"), terminal2=(bp_driver, "T0", 0)
        ),
        pf.parametric.route_taper(
            terminal1=(mrm_ref, "MSB-"), terminal2=(bp_driver, "T0", 1)
        ),
        pf.parametric.route_taper(
            terminal1=(mrm_ref, "LSB-"), terminal2=(bp_driver, "T0", 3)
        ),
        pf.parametric.route_taper(
            terminal1=(mrm_ref, "LSB+"), terminal2=(bp_driver, "T0", 4)
        ),
        pf.parametric.route_taper(
            terminal1=(mrm_ref, "H+"), terminal2=(bp_heater, "T0", 0), layer="M1_heater"
        ),
        pf.parametric.route_taper(
            terminal1=(mrm_ref, "H-"), terminal2=(bp_heater, "T0", 1), layer="M1_heater"
        ),
        pf.parametric.route_manhattan(
            terminal1=(bp_driver, "T0", 5),
            terminal2=(bp_heater, "T0", 2),
            width=40,
            direction2="x",
        ),
    )
    return c


mrm_bp = mrm_with_bond_pads()
viewer(mrm_bp)
[5]:
../_images/examples_Microring_Modulator_9_0.svg

Since we don’t know have the dimensions for the edge coupler, we’ll make a black box and simply add a waveguide port. An example of coupler design is available in another notebook, but it is not the main purpose fir this example.

[6]:
@pf.parametric_component(name_prefix="EC")
def edge_coupler(length=50, width=10):
    c = pf.Component()
    rect = pf.Rectangle((0, -width / 2), (length, width / 2))
    c.add("BlackBox", rect, "Text", pf.Label("Coupler", (length / 2, 0)))
    c.add_port(pf.Port((length, 0), 180, pf.config.default_kwargs["port_spec"]))
    return c


ec = edge_coupler()
ec
[6]:
../_images/examples_Microring_Modulator_11_0.svg

Making the final PIC

We can now make the final PIC by connecting multiple MRMs with edge couplers and the additional bond pads.

[7]:
main = pf.Component("MRM_PIC")

num_mrms = 4

# add copies of mrm with bond pads
mrm_ref = pf.Reference(mrm_bp, origin=(0, 0), columns=num_mrms, spacing=(1100, 0))
main.add(mrm_ref)

# Connect the mrms
for i in range(num_mrms - 1):
    p0 = mrm_ref["P1"][i].center
    p1 = mrm_ref["P0"][i + 1].center
    waypoints = [(p1[0] - pf.config.default_kwargs["radius"] * 2, -200, 0)]
    main.add(
        pf.parametric.route(
            port1=(mrm_ref, "P1", i),
            port2=(mrm_ref, "P0", i + 1),
            waypoints=waypoints,
        )
    )


# Add edge coupler
ec1_ref = pf.Reference(ec, (mrm_ref["P0"][0].center[0] - 1000, -200))
ec2_ref = pf.Reference(ec, (mrm_ref["P1"][-1].center[0] + 1000, -200), rotation=180)

main.add(
    ec1_ref,
    ec2_ref,
    pf.parametric.route(
        port2=(ec1_ref, "P0"),
        port1=(mrm_ref, "P0", 0),
    ),
    pf.parametric.route(
        port2=(ec2_ref, "P0"),
        port1=(mrm_ref, "P1", num_mrms - 1),
    ),
)

# Add two bond pads and a straight connector in between
bp_ref1 = pf.Reference(bp, (-450, 350))
bp_ref2 = pf.Reference(bp, (-450, -200))
main.add(
    bp_ref1,
    bp_ref2,
    pf.parametric.route_manhattan(
        terminal1=(bp_ref1, "T0"), terminal2=(bp_ref2, "T0"), width=40
    ),
)

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


viewer(main)
[7]:
../_images/examples_Microring_Modulator_13_0.svg