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.
import numpy as npimport photonforge as pfimport siepic_forge as siepicfrom photonforge.live_viewer import LiveViewer
viewer = LiveViewer()LiveViewer started at http://127.0.0.1:34211. Call its 'stop()' method to free the port.Choose default technology and keyword arguments for parametric components.
tech = siepic.ebeam()
pf.config.default_technology = techpf.config.default_kwargs["radius"] = 40pf.config.default_kwargs["euler_fracton"] = 0.5pf.config.default_kwargs["port_spec"] = tech.ports["TE_1550_500"]
pf.config.svg_labels = FalseWe crate the ring as a parametric component with a few adjustable parameters:
@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)The bond pads can be easily created as a static component:
bp = pf.Component("BondPad")bp.add_terminal( pf.Terminal("M2_router", pf.Rectangle(size=(100, 100))), add_structure=True)bp.add("M_Open", pf.Rectangle(size=(95, 95)))bpNow we combine a single ring with the bond pads:
@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)Since the dimensions of the edge coupler are not known, we make it a black box and simply add a waveguide port. Designing such a coupler is outside the scope of this example; the Edge Coupler example covers it in detail.
@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( "Dream Photonics Black Box-Not Fabricated", 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()ecMaking the final PIC
Section titled “Making the final PIC”We can now make the final PIC by connecting multiple MRMs with edge couplers and the additional bond pads.
main = pf.Component("MRM_PIC")
num_mrms = 4
# add copies of mrm with bond padsmrm_ref = pf.Reference(mrm_bp, origin=(0, 0), columns=num_mrms, spacing=(1100, 0))main.add(mrm_ref)
# Connect the mrmsmain.add( pf.parametric.route_z(port1=(mrm_ref, "P1", i), port2=(mrm_ref, "P0", i + 1)) for i in range(num_mrms - 1))
# Add edge couplerec1_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_z(port1=(mrm_ref, "P0", 0), port2=(ec1_ref, "P0")), pf.parametric.route_z(port1=(mrm_ref, "P1", num_mrms - 1), port2=(ec2_ref, "P0")),)
# Add two bond pads and a straight connector in betweenbp_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 regionchip_bounds = pf.envelope(main, 200, use_box=True, trim_x_max=True, trim_x_min=True)main.add("DevRec", chip_bounds)
viewer(main)