Source code for lnoi400.cells

from functools import partial

import gdsfactory as gf
import numpy as np
from gdsfactory.routing import route_quad
from gdsfactory.typings import ComponentSpec, CrossSectionSpec

from lnoi400.spline import (
    bend_S_spline,
    bend_S_spline_varying_width,
    spline_clamped_path,
)
from lnoi400.tech import LAYER, xs_uni_cpw

################
# Straights
################


@gf.cell
def _straight(
    length: float = 10.0,
    cross_section: CrossSectionSpec = "xs_rwg1000",
) -> gf.Component:
    return gf.components.straight(
        length=length,
        cross_section=cross_section,
    )


[docs] @gf.cell def straight_rwg1000(length: float = 10.0, **kwargs) -> gf.Component: """Straight single-mode waveguide.""" if "cross_section" not in kwargs: kwargs["cross_section"] = "xs_rwg1000" return _straight( length=length, **kwargs, )
[docs] @gf.cell def straight_rwg3000(length: float = 10.0, **kwargs) -> gf.Component: """Straight multimode waveguide.""" if "cross_section" not in kwargs: kwargs["cross_section"] = "xs_rwg3000" return _straight( length=length, **kwargs, )
########## # Bends ##########
[docs] @gf.cell def L_turn_bend( radius: float = 80.0, p: float = 1.0, with_arc_floorplan: bool = True, cross_section: CrossSectionSpec = "xs_rwg1000", **kwargs, ) -> gf.Component: """ A 90-degrees bend following an Euler path, with linearly-varying curvature (increasing and decreasing). """ npoints = int(np.round(200 * radius / 80.0)) angle = 90.0 return gf.components.bend_euler( radius=radius, angle=angle, p=p, with_arc_floorplan=with_arc_floorplan, npoints=npoints, cross_section=cross_section, **kwargs, )
# TODO: inquire about meaning of bend_points_distance in relation with Euler bends
[docs] @gf.cell def U_bend_racetrack( v_offset: float = 90.0, p: float = 1.0, with_arc_floorplan: bool = True, cross_section: CrossSectionSpec = "xs_rwg3000", **kwargs, ) -> gf.Component: """A U-bend with fixed cross-section and dimensions, suitable for building a low-loss racetrack resonator.""" radius = 0.5 * v_offset npoints = int(np.round(600 * radius / 90.0)) angle = 180.0 return gf.components.bend_euler( radius=radius, angle=angle, p=p, with_arc_floorplan=with_arc_floorplan, npoints=npoints, cross_section=cross_section, **kwargs, )
[docs] @gf.cell def S_bend_vert( v_offset: float = 25.0, h_extent: float = 100.0, dx_straight: float = 5.0, cross_section: CrossSectionSpec = "xs_rwg1000", ) -> gf.Component: """A spline bend that bridges a vertical displacement.""" if np.abs(v_offset) < 10.0: raise ValueError( f"The vertical distance bridged by the S-bend ({v_offset}) is too small." ) if np.abs(h_extent / v_offset) < 3.5 or h_extent < 90.0: raise ValueError( f"The bend would be too tight. Increase h_extent from its current value of {h_extent}." ) S_bend = gf.components.extend_ports( bend_S_spline( size=(h_extent, v_offset), cross_section=cross_section, npoints=int(np.round(2.5 * h_extent)), path_method=spline_clamped_path, ), length=dx_straight, cross_section=cross_section, ) bend_cell = gf.Component() bend_ref = bend_cell << S_bend bend_ref.dmove(bend_ref.ports["o1"].dcenter, (0.0, 0.0)) bend_cell.add_port(name="o1", port=bend_ref.ports["o1"]) bend_cell.add_port(name="o2", port=bend_ref.ports["o2"]) bend_cell.flatten() return bend_cell
################ # MMIs ################
[docs] @gf.cell def mmi1x2_optimized1550( width_mmi: float = 6.0, length_mmi: float = 26.75, width_taper: float = 1.5, length_taper: float = 25.0, port_ratio: float = 0.55, cross_section: CrossSectionSpec = "xs_rwg1000", **kwargs, ) -> gf.Component: """MMI1x2 with layout optimized for maximum transmission at 1550 nm.""" gap_mmi = ( port_ratio * width_mmi - width_taper ) # The port ratio is defined as the ratio between the waveguides separation and the MMI width. return gf.components.mmi1x2( width_mmi=width_mmi, length_mmi=length_mmi, gap_mmi=gap_mmi, length_taper=length_taper, width_taper=width_taper, cross_section=cross_section, **kwargs, )
[docs] @gf.cell def mmi2x2optimized1550( width_mmi: float = 5.0, length_mmi: float = 76.5, width_taper: float = 1.5, length_taper: float = 25.0, port_ratio: float = 0.7, cross_section: CrossSectionSpec = "xs_rwg1000", **kwargs, ) -> gf.Component: """MMI2x2 with layout optimized for maximum transmission at 1550 nm.""" gap_mmi = ( port_ratio * width_mmi - width_taper ) # The port ratio is defined as the ratio between the waveguides separation and the MMI width. return gf.components.mmi2x2( width_mmi=width_mmi, length_mmi=length_mmi, gap_mmi=gap_mmi, length_taper=length_taper, width_taper=width_taper, cross_section=cross_section, **kwargs, )
mmi2x2_optimized1550 = mmi2x2optimized1550 ##################### # Directional coupler #####################
[docs] @gf.cell def directional_coupler_balanced( io_wg_sep: float = 30.6, sbend_length: float = 58, central_straight_length: float = 16.92, coupl_wg_sep: float = 0.8, coup_wg_width: float = 0.8, cross_section_io: CrossSectionSpec = "xs_rwg1000", ) -> gf.Component: """Returns a 50-50 directional coupler. Default parameters give a 50/50 splitting at 1550 nm. Args: io_wg_sep: Separation of the two straights at the input/output, top-to-top. sbend_length: length of the s-bend part. central_straight_length: length of the coupling region. coupl_wg_sep: Distance between two waveguides in the coupling region (side to side). cross_section_io: cross section spec at the i/o (must be defined in tech.py). coup_wg_width: waveguide width at the coupling section. """ s0 = gf.Section( width=coup_wg_width, offset=0, layer="LN_RIDGE", name="_default", port_names=("o1", "o2"), ) s1 = gf.Section(width=10.0, offset=0, layer="LN_SLAB", name="slab", simplify=0.03) cross_section_coupling = gf.CrossSection(sections=[s0, s1]) cross_section_io = gf.get_cross_section(cross_section_io) s_height = ( io_wg_sep - coupl_wg_sep - coup_wg_width ) / 2 # take into account the width of the waveguide size = (sbend_length, s_height) # s-bend settings settings_s_bend = { "size": size, "cross_section1": cross_section_coupling, "cross_section2": cross_section_io, "npoints": 201, } dc = gf.Component() # top right branch c_tr = dc << bend_S_spline_varying_width(**settings_s_bend) c_tr.dmove( c_tr.ports["o1"].dcenter, (central_straight_length / 2, 0.5 * (coupl_wg_sep + coup_wg_width)), ) # bottom right branch c_br = dc << bend_S_spline_varying_width(**settings_s_bend) c_br.dmirror_y() c_br.dmove( c_br.ports["o1"].dcenter, (central_straight_length / 2, -0.5 * (coupl_wg_sep + coup_wg_width)), ) # central waveguides straight_center_up = dc << gf.components.straight( length=central_straight_length, cross_section=cross_section_coupling ) straight_center_up.connect("o2", c_tr.ports["o1"]) straight_center_down = dc << gf.components.straight( length=central_straight_length, cross_section=cross_section_coupling ) straight_center_down.connect("o2", c_br.ports["o1"]) # top left branch c_tl = dc << bend_S_spline_varying_width(**settings_s_bend) c_tl.dmirror_x() c_tl.dmove(c_tl.ports["o1"].dcenter, straight_center_up.ports["o1"].dcenter) # bottom left branch c_bl = dc << bend_S_spline_varying_width(**settings_s_bend) c_bl.dmirror_x() c_bl.dmirror_y() c_bl.dmove(c_bl.ports["o1"].dcenter, straight_center_down.ports["o1"].dcenter) # Expose the ports exposed_ports = [ ("o1", c_bl.ports["o2"]), ("o2", c_tl.ports["o2"]), ("o3", c_tr.ports["o2"]), ("o4", c_br.ports["o2"]), ] [dc.add_port(name=name, port=port) for name, port in exposed_ports] return dc
################ # Edge couplers ################
[docs] @gf.cell def double_linear_inverse_taper( cross_section_start: CrossSectionSpec = "xs_swg250", cross_section_end: CrossSectionSpec = "xs_rwg1000", lower_taper_length: float = 120.0, lower_taper_end_width: float = 2.05, upper_taper_start_width: float = 0.25, upper_taper_length: float = 240.0, slab_removal_width: float = 20.0, input_ext: float = 0.0, ) -> gf.Component: """Inverse taper with two layers, starting from a wire waveguide at the facet and transitioning to a rib waveguide. The tapering profile is linear in both layers.""" lower_taper_start_width = gf.get_cross_section(cross_section_start).width upper_taper_end_width = gf.get_cross_section(cross_section_end).width xs_taper_lower_end = partial( gf.cross_section.strip, width=lower_taper_start_width + (lower_taper_end_width - lower_taper_start_width) * (1 + upper_taper_length / lower_taper_length), layer="LN_SLAB", ) xs_taper_upper_start = partial( gf.cross_section.strip, layer=LAYER.LN_RIDGE, width=upper_taper_start_width ) xs_taper_upper_end = partial(xs_taper_upper_start, width=upper_taper_end_width) taper_lower = gf.components.taper_cross_section( cross_section1=cross_section_start, cross_section2=xs_taper_lower_end, length=lower_taper_length + upper_taper_length, linear=True, ) taper_upper = gf.components.taper_cross_section( cross_section1=xs_taper_upper_start, cross_section2=xs_taper_upper_end, length=upper_taper_length, linear=True, ) if input_ext: straight_ext = gf.components.straight( cross_section=cross_section_start, length=input_ext, ) # Place the two tapers on the different layers double_taper = gf.Component() if input_ext: sref = double_taper << straight_ext sref.dmovex(-input_ext) ltref = double_taper << taper_lower utref = double_taper << taper_upper utref.dmovex(lower_taper_length) # Define the input and output optical ports double_taper.add_port( port=sref.ports["o1"] ) if input_ext else double_taper.add_port(port=ltref.ports["o1"]) double_taper.add_port(port=utref.ports["o2"]) # Place the tone inversion box for the slab etch if slab_removal_width: bn = gf.components.rectangle( size=( double_taper.ports["o2"].dcenter[0] - double_taper.ports["o1"].dcenter[0], slab_removal_width, ), centered=True, layer=LAYER.SLAB_NEGATIVE, ) bnref = double_taper << bn bnref.dmovex( origin=bnref.dxmin, destination=-input_ext, ) double_taper.flatten() return double_taper
################### # GSG bonding pad ###################
[docs] @gf.cell def CPW_pad_linear( start_width: float = 80.0, length_straight: float = 10.0, length_tapered: float = 190.0, cross_section: CrossSectionSpec = "xs_uni_cpw", ) -> gf.Component: """RF access line for high-frequency GSG probes. The probe pad maintains a fixed gap/central conductor ratio across its length, to achieve a good impedance matching""" xs_cpw = gf.get_cross_section(cross_section) # Extract the CPW cross sectional parameters sections = xs_cpw.sections signal_section = [s for s in sections if s.name == "signal"][0] ground_section = [s for s in sections if s.name == "ground_top"][0] end_width = signal_section.width ground_planes_width = ground_section.width end_gap = ground_section.offset - 0.5 * (end_width + ground_planes_width) aspect_ratio = end_width / (end_width + 2 * end_gap) # Pad elements generation pad = gf.Component() start_gap = 0.5 * (aspect_ratio ** (-1) - 1) * start_width central_conductor_shape = [ (0.0, start_width / 2.0), (length_straight, start_width / 2.0), (length_straight + length_tapered, end_width / 2.0), (length_straight + length_tapered, -end_width / 2.0), (length_straight, -start_width / 2.0), (0.0, -start_width / 2.0), ] ground_plane_shape = [ (0.0, start_width / 2.0 + start_gap), (length_straight, start_width / 2.0 + start_gap), (length_straight + length_tapered, end_width / 2.0 + end_gap), ( length_straight + length_tapered, end_width / 2.0 + end_gap + ground_planes_width, ), (0.0, end_width / 2.0 + end_gap + ground_planes_width), ] bottom_ground_shape = gf.Path(ground_plane_shape).dmirror((0, 0), (1, 0)) pad.add_polygon(central_conductor_shape, layer="TL") pad.add_polygon(ground_plane_shape, layer="TL") pad.add_polygon(bottom_ground_shape.points, layer="TL") # Ports definition pad.add_port( name="e1", center=(length_straight, 0.0), width=start_width, orientation=180.0, port_type="electrical", layer="TL", ) pad.add_port( name="e2", center=(length_straight + length_tapered, 0.0), width=end_width, orientation=0.0, port_type="electrical", layer="TL", ) return pad
#################### # Transmission lines ####################
[docs] @gf.cell() def uni_cpw_straight( length: float = 3000.0, cross_section: CrossSectionSpec = "xs_uni_cpw", bondpad: ComponentSpec = "CPW_pad_linear", ) -> gf.Component: """A CPW transmission line for microwaves, with a uniform cross section.""" cpw = gf.Component() bp = gf.get_component(bondpad, cross_section=cross_section) tl = cpw << gf.components.straight(length=length, cross_section=cross_section) bp1 = cpw << bp bp2 = cpw << bp bp1.connect("e2", tl.ports["e1"]) bp2.dmirror() bp2.connect("e2", tl.ports["e2"]) cpw.add_ports(tl.ports) cpw.add_port( name="bp1", port=bp1.ports["e1"], ) cpw.add_port( name="bp2", port=bp2.ports["e1"], ) cpw.flatten() return cpw
################### # Thermal shifters ###################
[docs] @gf.cell def heater_resistor( path: gf.path.Path | None = None, width: float = 0.9, offset: float = 0.0, ) -> gf.Component: """A resistive wire used as a low-frequency phase shifter, exploiting the thermo-optical effect.""" if not path: path = gf.path.straight(length=150.0) xs = gf.get_cross_section("xs_ht_wire", width=width, offset=offset) c = path.extrude(xs) return c
[docs] @gf.cell def heater_straight_single( length: float = 150.0, width: float = 0.9, offset: float = 0.0, port_contact_width_ratio: float = 3.0, pad_size: tuple[float, float] = (100.0, 100.0), pad_pitch: float | None = None, pad_vert_offset: float = 10.0, ) -> gf.Component: """A straight resistive wire used as a low-frequency phase shifter, exploiting the thermo-optical effect. The heater is terminated by wide pads for probing or bonding.""" if pad_vert_offset <= 0: raise ValueError( "pad_vert_offset must be a positive number," + f"received {pad_vert_offset}." ) if port_contact_width_ratio <= 0: raise ValueError( "port_contact_width_ratio must be a positive number," + f"received {port_contact_width_ratio}." ) if not pad_pitch: pad_pitch = length c = gf.Component() bondpads = gf.components.pad_array( pad=gf.components.pad, size=pad_size, column_pitch=pad_pitch, row_pitch=pad_pitch, columns=2, port_orientation=-90.0, layer=LAYER.HT, ) bps = c << bondpads ht = heater_resistor( path=gf.path.straight(length), width=width, offset=offset, ) # Place the ports along the edge of the wire for p in ht.ports: if p.orientation == 0.0: p.dcenter = (p.dcenter[0] - 0.5 * p.dwidth, p.dcenter[1] + 0.5 * width) if p.orientation == 180.0: p.dcenter = (p.dcenter[0] + 0.5 * p.dwidth, p.dcenter[1] + 0.5 * width) p.orientation = 90.0 ht_ref = c << ht bps.dcenter = [ht_ref.dcenter.x, bps.dcenter.y] bps.dymin = ht_ref.dymax + pad_vert_offset port_contact_width = port_contact_width_ratio * width ht.ports["e1"].dx += 0.5 * (port_contact_width - width) ht.ports["e2"].dx -= 0.5 * (port_contact_width - width) routing_params = { "width2": port_contact_width, "layer": LAYER.HT, } # Connect pads and heater wire _ = route_quad( c, port1=bps.ports["e11"], port2=ht.ports["e1"], **routing_params, ) _ = route_quad( c, port1=bps.ports["e12"], port2=ht.ports["e2"], **routing_params, ) c.add_port( name="ht_start", port=ht.ports["e1"], ) c.add_port( name="ht_end", port=ht.ports["e2"], ) c.add_port( name="e1", port=bps.ports["e11"], ) c.add_port( name="e2", port=bps.ports["e12"], ) c.flatten() return c
############### # Modulators ###############
[docs] @gf.cell def eo_phase_shifter( rib_core_width_modulator: float = 2.5, taper_length: float = 100.0, modulation_length: float = 7500.0, rf_central_conductor_width: float = 10.0, rf_ground_planes_width: float = 180.0, rf_gap: float = 4.0, draw_cpw: bool = True, ) -> gf.Component: """Phase shifter based on the Pockels effect. The waveguide is located within the gap of a CPW transmission line.""" ps = gf.Component() xs_modulator = gf.get_cross_section("xs_rwg1000", width=rib_core_width_modulator) wg_taper = gf.components.taper_cross_section( cross_section1="xs_rwg1000", cross_section2=xs_modulator, length=taper_length ) wg_phase_modulation = gf.components.straight( length=modulation_length - 2 * taper_length, cross_section=xs_modulator ) taper_1 = ps << wg_taper wg_pm = ps << wg_phase_modulation taper_2 = ps << wg_taper taper_2.dmirror_x() wg_pm.connect("o1", taper_1.ports["o2"]) taper_2.dmirror_x() taper_2.connect("o2", wg_pm.ports["o2"]) for name, port in [ ("o1", taper_1.ports["o1"]), ("o2", taper_2.ports["o1"]), ]: ps.add_port(name=name, port=port) # Add the transmission line if draw_cpw: xs_cpw = gf.partial( xs_uni_cpw, central_conductor_width=rf_central_conductor_width, ground_planes_width=rf_ground_planes_width, gap=rf_gap, ) tl = ps << gf.components.straight( length=modulation_length, cross_section=xs_cpw ) tl.dmove( tl.ports["e1"].dcenter, (0.0, -0.5 * rf_central_conductor_width - 0.5 * rf_gap), ) for name, port in [ ("e1", tl.ports["e1"]), ("e2", tl.ports["e2"]), ]: ps.add_port(name=name, port=port) ps.flatten() return ps
@gf.cell def _mzm_interferometer( splitter: ComponentSpec = "mmi1x2_optimized1550", taper_length: float = 100.0, rib_core_width_modulator: float = 2.5, modulation_length: float = 7500.0, length_imbalance: float = 100.0, bias_tuning_section_length: float = 750.0, sbend_large_size: tuple[float, float] = (200.0, 50.0), sbend_small_size: tuple[float, float] = (200.0, -45.0), sbend_small_straight_extend: float = 5.0, lbend_tune_arm_reff: float = 75.0, lbend_combiner_reff: float = 80.0, ) -> gf.Component: interferometer = gf.Component() sbend_large = S_bend_vert( v_offset=sbend_large_size[1], h_extent=sbend_large_size[0], dx_straight=5.0 ) sbend_small = S_bend_vert( v_offset=sbend_small_size[1], h_extent=sbend_small_size[0], dx_straight=sbend_small_straight_extend, ) def branch_top(): bt = gf.Component() sbend_1 = bt << sbend_large sbend_2 = bt << sbend_small pm = bt << eo_phase_shifter( rib_core_width_modulator=rib_core_width_modulator, modulation_length=modulation_length, taper_length=taper_length, draw_cpw=False, ) sbend_3 = bt << sbend_small sbend_2.connect("o1", sbend_1.ports["o2"]) pm.connect("o1", sbend_2.ports["o2"]) sbend_3.dmirror_x() sbend_3.connect("o1", pm.ports["o2"]) for name, port in [ ("o1", sbend_1.ports["o1"]), ("o2", sbend_3.ports["o2"]), ("taper_start", pm.ports["o1"]), ]: bt.add_port(name=name, port=port) bt.flatten() return bt def branch_tune_short(straight_unbalance: float = 0.0): arm = gf.Component() lbend = L_turn_bend(radius=lbend_tune_arm_reff) straight_y = gf.components.straight( length=20.0 + straight_unbalance, cross_section="xs_rwg1000" ) straight_x = gf.components.straight( length=bias_tuning_section_length, cross_section="xs_rwg1000" ) symbol_to_component = { "b": (lbend, "o1", "o2"), "L": (straight_y, "o1", "o2"), "B": (lbend, "o2", "o1"), "_": (straight_x, "o1", "o2"), } sequence = "bLB_!b!L" arm = gf.components.component_sequence( sequence=sequence, ports_map={"phase_tuning_segment_start": ("_1", "o1")}, symbol_to_component=symbol_to_component, ) arm.add_port(port=arm.ports["phase_tuning_segment_start"]) arm.flatten() return arm def branch_tune_long(straight_unbalance): return partial(branch_tune_short, straight_unbalance=straight_unbalance)() splt = gf.get_component(splitter) # Uniformly handle the cases of a 1x2 or 2x2 MMI if "2x2" in splitter: out_top = splt.ports["o3"] out_bottom = splt.ports["o4"] elif "1x2" in splitter: out_top = splt.ports["o2"] out_bottom = splt.ports["o3"] else: raise ValueError(f"Splitter cell {splitter} not supported.") def combiner_section(): comb_section = gf.Component() lbend_combiner = L_turn_bend(radius=lbend_combiner_reff) lbend_top = comb_section << lbend_combiner lbend_bottom = comb_section << lbend_combiner lbend_bottom.dmirror_y() combiner = comb_section << splt lbend_top.connect("o1", out_top) lbend_bottom.connect("o1", out_bottom) # comb_section.flatten() exposed_ports = [ ("o2", lbend_top.ports["o2"]), ("o1", combiner.ports["o1"]), ("o3", lbend_bottom.ports["o2"]), ] if "2x2" in splitter: exposed_ports.append( ("in2", combiner.ports["o2"]), ) for name, port in exposed_ports: comb_section.add_port(name=name, port=port) return comb_section splt_ref = interferometer << splt bt = interferometer << branch_top() bb = interferometer << branch_top() bs = interferometer << branch_tune_short() bl = interferometer << branch_tune_long(abs(0.5 * length_imbalance)) cs = interferometer << combiner_section() bb.dmirror_y() bt.connect("o1", out_top) bb.connect("o1", out_bottom) if length_imbalance >= 0: bs.dmirror_y() bs.connect("o1", bb.ports["o2"]) bl.connect("o1", bt.ports["o2"]) else: bs.connect("o1", bt.ports["o2"]) bl.dmirror_y() bl.connect("o1", bb.ports["o2"]) cs.dmirror_x() [ cs.connect("o2", bl.ports["o2"]) if length_imbalance >= 0 else cs.connect("o2", bs.ports["o2"]) ] exposed_ports = [ ("o1", splt_ref.ports["o1"]), ("upper_taper_start", bt.ports["taper_start"]), ("short_bias_branch_start", bs.ports["phase_tuning_segment_start"]), ("long_bias_branch_start", bl.ports["phase_tuning_segment_start"]), ("o2", cs.ports["o1"]), ] if "2x2" in splitter: exposed_ports.extend( [ ("out2", cs.ports["in2"]), ("in2", splt_ref.ports["o2"]), ] ) for name, port in exposed_ports: interferometer.add_port(name=name, port=port) interferometer.flatten() return interferometer
[docs] @gf.cell def mzm_unbalanced( modulation_length: float = 7500.0, length_imbalance: float = 100.0, lbend_tune_arm_reff: float = 75.0, rf_pad_start_width: float = 80.0, rf_central_conductor_width: float = 10.0, rf_ground_planes_width: float = 180.0, rf_gap: float = 4.0, rf_pad_length_straight: float = 10.0, rf_pad_length_tapered: float = 190.0, bias_tuning_section_length: float = 700.0, with_heater: bool = False, heater_offset: float = 1.2, heater_width: float = 1.0, heater_pad_size: tuple[float, float] = (75.0, 75.0), **kwargs, ) -> gf.Component: """Mach-Zehnder modulator based on the Pockels effect with an applied RF electric field. The modulator works in a differential push-pull configuration driven by a single GSG line.""" mzm = gf.Component() # Transmission line subcell xs_cpw = gf.partial( xs_uni_cpw, central_conductor_width=rf_central_conductor_width, ground_planes_width=rf_ground_planes_width, gap=rf_gap, ) rf_line = mzm << uni_cpw_straight( bondpad={ "component": "CPW_pad_linear", "settings": { "start_width": rf_pad_start_width, "length_straight": rf_pad_length_straight, "length_tapered": rf_pad_length_tapered, }, }, length=modulation_length, cross_section=xs_cpw(), ) rf_line.dmove(rf_line.ports["e1"].dcenter, (0.0, 0.0)) # Interferometer subcell if "splitter" not in kwargs.keys(): kwargs["splitter"] = "mmi1x2_optimized1550" splitter = kwargs["splitter"] splitter = gf.get_component(splitter) sbend_large_AR = 3.6 GS_separation = rf_pad_start_width * rf_gap / rf_central_conductor_width sbend_large_v_offset = ( 0.5 * rf_pad_start_width + 0.5 * GS_separation - 0.5 * splitter.settings["port_ratio"] * splitter.settings["width_mmi"] ) sbend_small_straight_length = rf_pad_length_straight * 0.5 lbend_combiner_reff = ( 0.5 * rf_pad_start_width + lbend_tune_arm_reff + 0.5 * GS_separation - 0.5 * splitter.settings["port_ratio"] * splitter.settings["width_mmi"] ) interferometer = ( mzm << partial( _mzm_interferometer, modulation_length=modulation_length, length_imbalance=length_imbalance, sbend_large_size=( sbend_large_AR * sbend_large_v_offset, sbend_large_v_offset, ), sbend_small_size=( rf_pad_length_straight + rf_pad_length_tapered - 2 * sbend_small_straight_length, -0.5 * ( rf_pad_start_width - rf_central_conductor_width + GS_separation - rf_gap ), ), sbend_small_straight_extend=sbend_small_straight_length, lbend_tune_arm_reff=lbend_tune_arm_reff, lbend_combiner_reff=lbend_combiner_reff, bias_tuning_section_length=bias_tuning_section_length, **kwargs, )() ) interferometer.dmove( interferometer.ports["upper_taper_start"].dcenter, (0.0, 0.5 * (rf_central_conductor_width + rf_gap)), ) # Add heater for phase tuning if with_heater: ht_ref = mzm << heater_straight_single( length=bias_tuning_section_length, width=heater_width, offset=heater_offset, pad_size=heater_pad_size, ) if length_imbalance < 0.0: heater_disp = [0, 0.5 * heater_width + heater_offset] else: ht_ref.dmirror_y() heater_disp = [0, -0.5 * heater_width - heater_offset] ht_ref.dmove( origin=ht_ref.ports["ht_start"].dcenter, destination=( np.array(interferometer.ports["long_bias_branch_start"].dcenter) + heater_disp ), ) # Expose the ports exposed_ports = [ ("e1", rf_line.ports["e1"]), ("e2", rf_line.ports["e2"]), ] if "1x2" in kwargs["splitter"]: exposed_ports.extend( [ ("o1", interferometer.ports["o1"]), ("o2", interferometer.ports["o2"]), ] ) elif "2x2" in kwargs["splitter"]: exposed_ports.extend( [ ("o1", interferometer.ports["o1"]), ("o2", interferometer.ports["in2"]), ("o3", interferometer.ports["out2"]), ("o4", interferometer.ports["o2"]), ] ) if with_heater: exposed_ports += [ ("e3", ht_ref.ports["e1"]), ( "e4", ht_ref.ports["e2"], ), ] [mzm.add_port(name=name, port=port) for name, port in exposed_ports] return mzm
################## # Chip floorplan ##################
[docs] @gf.cell def chip_frame( size: tuple[float, float] = (10_000, 5000), exclusion_zone_width: float = 50, center: tuple[float, float] = None, ) -> gf.Component: """Provide the chip extent and the exclusion zone around the chip frame. In the exclusion zone, only the edge couplers routing to the chip facet should be placed. Allowed chip dimensions (in either direction): 5000 um, 10000 um, 20000 um.""" # Check that the chip dimensions have the admissible values. snapped_size = [] if size[0] <= 5050 and size[1] <= 5050: raise (ValueError(f"The chip frame size {size} is not supported.")) if size[0] > 20200 or size[1] > 20200: raise (ValueError(f"The chip frame size {size} is not supported.")) else: for s in size: if abs(s - 5000.0) <= 50.0: snapped_size.append(4950.0) elif abs(s - 10000.0) <= 100.0: snapped_size.append(10000) elif abs(s - 20000.0) <= 200: snapped_size.append(20100) else: raise (ValueError(f"The chip frame size {size} is not supported.")) # Chip frame elements inner_box = gf.components.rectangle( size=tuple(snapped_size), layer=LAYER.CHIP_CONTOUR, centered=True, ) outer_box = gf.components.rectangle( size=tuple(s + 2 * exclusion_zone_width for s in snapped_size), layer=LAYER.CHIP_EXCLUSION_ZONE, centered=True, ) c = gf.Component() ib = c << inner_box ob = c << outer_box if center: ib.dmove(origin=(0.0, 0.0), destination=center) ob.dmove(origin=(0.0, 0.0), destination=center) c.flatten() return c
if __name__ == "__main__": mzm1 = mzm_unbalanced( splitter="mmi1x2_optimized1550", length_imbalance=1200, with_heater=True, ) mzm1.show()