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 _utils.chip_floorplan import chip_frame
from _utils.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",
**kwargs,
) -> gf.Component:
return gf.components.straight(
length=length,
cross_section=cross_section,
**kwargs,
)
[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 = [(p[0], -p[1]) for p in ground_plane_shape]
pad.add_polygon(central_conductor_shape, layer="TL")
pad.add_polygon(ground_plane_shape, layer="TL")
pad.add_polygon(bottom_ground_shape, 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 = 1000.0,
cross_section: CrossSectionSpec = "xs_uni_cpw",
signal_width: float = 10.0,
gap_width: float = 4.0,
ground_planes_width: float = 250.0,
bondpad: ComponentSpec = "CPW_pad_linear",
) -> gf.Component:
"""A CPW transmission line for microwaves, with a uniform cross section."""
cpw_xs = gf.get_cross_section(
cross_section,
central_conductor_width=signal_width,
gap=gap_width,
ground_planes_width=ground_planes_width,
)
cpw = gf.Component()
bp = gf.get_component(bondpad, cross_section=cpw_xs)
tl = cpw << gf.components.straight(length=length, cross_section=cpw_xs)
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
[docs]
@gf.cell()
def trail_cpw(
length: float = 1000.0,
signal_width: float = 21,
gap_width: float = 4,
th: float = 1.5,
tl: float = 44.7,
tw: float = 7.0,
tt: float = 1.5,
tc: float = 5.0,
ground_planes_width: float = 180.0,
rounding_radius: float = 0.5,
bondpad: ComponentSpec = "CPW_pad_linear",
cross_section: CrossSectionSpec = xs_uni_cpw,
) -> gf.Component:
"""A CPW transmission line with periodic T-rails on all electrodes."""
num_cells = np.floor(length / (tl + tc))
gap_width_corrected = gap_width + 2 * th + 2 * tt # total gap width with T-rails
# redefine cross section to include T-rails
xs_cpw_trail = partial(
cross_section,
central_conductor_width=signal_width,
gap=gap_width_corrected,
ground_planes_width=ground_planes_width,
)
cpw = gf.Component()
bp = gf.get_component(bondpad, cross_section=xs_cpw_trail)
strght = cpw << gf.components.straight(length=length, cross_section=xs_cpw_trail)
bp1 = cpw << bp
bp2 = cpw << bp
bp1.connect("e2", strght.ports["e1"])
bp2.dmirror()
bp2.connect("e2", strght.ports["e2"])
cpw.add_ports(strght.ports)
cpw.add_port(
name="bp1",
port=bp1.ports["e1"],
)
cpw.add_port(
name="bp2",
port=bp2.ports["e1"],
)
# Initiate T-rail polygon element. Create a bit more to ensure round corners close to electrodes
trailpol = gf.kdb.DPolygon(
[
(tl, signal_width / 2),
(tl, signal_width / 2 - tt),
(0, signal_width / 2 - tt),
(0, signal_width / 2),
(tl / 2 - tw / 2, signal_width / 2),
(tl / 2 - tw / 2, signal_width / 2 + th),
(0, signal_width / 2 + th),
(0, signal_width / 2 + th + tt),
(tl, signal_width / 2 + th + tt),
(tl, signal_width / 2 + th),
(tl / 2 + tw / 2, signal_width / 2 + th),
(tl / 2 + tw / 2, signal_width / 2),
]
)
# Create T-rail component
trailcomp = gf.Component()
_ = trailcomp.add_polygon(trailpol, layer=cross_section().layer)
# Apply roc to the T-rail corners
trailround = gf.Component()
rinner = rounding_radius * 1000 # The circle radius of inner corners (in nm).
router = rounding_radius * 1000 # The circle radius of outer corners (in nm).
n = 30 # The number of points per full circle.
for layer, polygons in trailcomp.get_polygons().items():
for p in polygons:
p_round = p.round_corners(rinner, router, n)
trailround.add_polygon(p_round, layer=layer)
# Create T-rail unit cell
trail_uc = gf.Component()
inc_t1 = trail_uc << trailround
inc_t2 = trail_uc << trailround
inc_t2.dmovey(gap_width_corrected - th)
inc_t3 = trail_uc << trailround
inc_t3.dmovey(-signal_width - th)
inc_t4 = trail_uc << trailround
inc_t4.dmovey(-signal_width - gap_width_corrected)
# Place T-rails symmetrically w/r to bondpads
dl_tr = 0.5 * (length - num_cells * tl - (num_cells - 1) * tc)
[ref.dmovex(dl_tr) for ref in (inc_t1, inc_t2, inc_t3, inc_t4)]
# Duplicate cell
cpw.add_ref(
trail_uc,
columns=num_cells,
rows=1,
column_pitch=tl + tc,
)
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
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,
cpw_cell: ComponentSpec = uni_cpw_straight,
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 << cpw_cell(
length=modulation_length,
cross_section=xs_cpw,
gap_width=rf_gap,
signal_width=rf_central_conductor_width,
ground_planes_width=rf_ground_planes_width,
)
gap_eff = rf_gap + 2 * np.sum(
[tl.cell.settings[key] for key in ("tt", "th") if key in tl.cell.settings]
)
tl.dmove(
tl.ports["e1"].dcenter,
(0.0, -0.5 * rf_central_conductor_width - 0.5 * gap_eff),
)
for name, port in [
("e1", tl.ports["bp1"]),
("e2", tl.ports["bp2"]),
]:
ps.add_port(name=name, port=port)
ps.flatten()
return ps
[docs]
@gf.cell
def eo_phase_shifter_high_speed(**kwargs) -> gf.Component:
"""High-speed phase shifter based on the Pockels effect. The waveguide is located
within the gap of a CPW transmission line.
Note: The base variant (eo_phase_shifter) uses a default central conductor width of 10.0,
while this high-speed variant explicitly passes 21.0 for rf_central_conductor_width to achieve the desired high-speed properties.
Pass the parameter set of eo_phase_shifter to modify.
"""
kwargs.setdefault("rf_central_conductor_width", 21.0)
kwargs.setdefault("cpw_cell", trail_cpw)
ps = eo_phase_shifter(**kwargs)
ps.info["additional_settings"] = dict(ps.settings)
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 len(splt.ports) == 4:
out_top = splt.ports["o3"]
out_bottom = splt.ports["o4"]
elif len(splt.ports) == 3:
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 = 300.0,
bias_tuning_section_length: float = 700.0,
cpw_cell: ComponentSpec = uni_cpw_straight,
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 << cpw_cell(
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,
signal_width=rf_central_conductor_width,
cross_section=xs_cpw,
ground_planes_width=rf_ground_planes_width,
gap_width=rf_gap,
)
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
gap_eff = rf_gap + 2 * np.sum(
[
rf_line.cell.settings[key]
for key in ("tt", "th")
if key in rf_line.cell.settings
]
)
GS_separation = rf_pad_start_width * gap_eff / 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
- gap_eff
),
),
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 + gap_eff)),
)
# 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["bp1"]),
("e2", rf_line.ports["bp2"]),
]
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
[docs]
@gf.cell
def mzm_unbalanced_high_speed(**kwargs) -> gf.Component:
"""High-speed 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.
Note: The base variant (mzm_unbalanced) uses a default central conductor width of 10.0,
while this high-speed variant explicitly passes 21.0 for rf_central_conductor_width to achieve the desired high-speed properties.
Pass the parameter set of mzm_unbalanced to modify.
"""
kwargs.setdefault("rf_central_conductor_width", 21.0)
kwargs.setdefault("cpw_cell", trail_cpw)
mzm = mzm_unbalanced(**kwargs)
mzm.info["additional_settings"] = dict(mzm.settings)
return mzm
if __name__ == "__main__":
chip_frame().show()