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
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]:
The bond pads can be used directly from the PDK:
[4]:
bp = siepic.component("ebeam_BondPad")
bp
[4]:
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]:
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]:
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]: