from __future__ import annotations
from functools import partial
import numpy as np
from numpy import float64
import gdsfactory as gf
from gdsfactory.cell import cell
from gdsfactory.component import Component
from gdsfactory.components.bezier import (
bezier,
bezier_curve,
find_min_curv_bezier_control_points,
)
from gdsfactory.components.ellipse import ellipse
from gdsfactory.components.taper import taper
from gdsfactory.geometry.functions import path_length
from gdsfactory.typings import ComponentSpec, CrossSectionSpec, LayerSpec
def snap_to_grid(p: float, grid_per_unit: int = 1000) -> float64:
"""Round."""
return np.round(p * grid_per_unit) / grid_per_unit
[docs]
@cell
def crossing_arm(
r1: float = 3.0,
r2: float = 1.1,
w: float = 1.2,
L: float = 3.4,
layer_slab: LayerSpec = "SLAB150",
cross_section: CrossSectionSpec = "xs_sc",
) -> Component:
"""Returns crossing arm.
Args:
r1: ellipse radius1.
r2: ellipse radius2.
w: width in um.
L: length in um.
layer_slab: for the shallow etch.
cross_section: spec.
"""
c = Component()
layer_slab = gf.get_layer(layer_slab)
c << ellipse(radii=(r1, r2), layer=layer_slab)
xs = gf.get_cross_section(cross_section)
width = xs.width
layer_wg = gf.get_layer(xs.layer)
a = np.round(L + w / 2, 3)
h = width / 2
taper_pts = [
(-a, h),
(-w / 2, w / 2),
(w / 2, w / 2),
(a, h),
(a, -h),
(w / 2, -w / 2),
(-w / 2, -w / 2),
(-a, -h),
]
c.add_polygon(taper_pts, layer=layer_wg)
c.add_port(
name="o1",
center=(-a, 0),
orientation=180,
width=width,
layer=layer_wg,
cross_section=xs,
)
c.add_port(
name="o2",
center=(a, 0),
orientation=0,
width=width,
layer=layer_wg,
cross_section=xs,
)
return c
[docs]
@cell
def crossing(
arm: ComponentSpec = crossing_arm,
cross_section: CrossSectionSpec = "xs_sc",
) -> Component:
"""Waveguide crossing.
Args:
arm: arm spec.
cross_section: spec.
"""
c = Component()
arm = gf.get_component(arm)
arm_h = arm.ref()
arm_v = arm.ref(rotation=90)
port_id = 0
for i in [arm_h, arm_v]:
c.add(i)
c.absorb(i)
for p in i.ports.values():
c.add_port(name=port_id, port=p)
port_id += 1
c.auto_rename_ports()
return c
_taper = partial(taper, width2=2.5, length=3)
[docs]
@cell
def crossing_from_taper(taper=_taper) -> Component:
"""Returns Crossing based on a taper.
The default is a dummy taper.
Args:
taper: taper function.
"""
taper = gf.get_component(taper)
c = Component()
for i, a in enumerate([0, 90, 180, 270]):
_taper = taper.ref(position=(0, 0), port_id="o2", rotation=a)
c.add(_taper)
c.add_port(name=i, port=_taper.ports["o1"])
c.absorb(_taper)
c.auto_rename_ports()
return c
[docs]
@cell
def crossing_etched(
width: float = 0.5,
r1: float = 3.0,
r2: float = 1.1,
w: float = 1.2,
L: float = 3.4,
layer_wg: LayerSpec = "WG",
layer_slab: LayerSpec = "SLAB150",
) -> Component:
"""Waveguide crossing.
Full crossing has to be on WG layer (to start with a 220nm slab).
Then we etch the ellipses down to 150nm slabs and we keep linear taper at 220nm.
Args:
width: input waveguides width.
r1: radii.
r2: radii.
w: wide width.
L: length.
layer_wg: waveguide layer.
layer_slab: shallow etch layer.
"""
layer_wg = gf.get_layer(layer_wg)
layer_slab = gf.get_layer(layer_slab)
# Draw the ellipses
c = Component()
ellipse1 = c << ellipse(radii=(r1, r2), layer=layer_wg)
ellipse2 = c << ellipse(radii=(r2, r1), layer=layer_wg)
c.absorb(ellipse1)
c.absorb(ellipse2)
a = L + w / 2
h = width / 2
taper_cross_pts = [
(-a, h),
(-w / 2, w / 2),
(-h, a),
(h, a),
(w / 2, w / 2),
(a, h),
(a, -h),
(w / 2, -w / 2),
(h, -a),
(-h, -a),
(-w / 2, -w / 2),
(-a, -h),
]
c.add_polygon(taper_cross_pts, layer=layer_wg)
# tapers_poly = c.add_polygon(taper_cross_pts, layer=layer_wg)
# b = a - 0.1 # To make sure we get 4 distinct polygons when doing bool ops
# tmp_polygon = [(-b, b), (b, b), (b, -b), (-b, -b)]
# polys_etch = gdstk.fast_boolean([tmp_polygon], tapers_poly, "not", layer=layer_slab)
# c.add(polys_etch)
positions = [(a, 0), (0, a), (-a, 0), (0, -a)]
angles = [0, 90, 180, 270]
for i, (p, angle) in enumerate(zip(positions, angles)):
c.add_port(
name=str(i),
center=p,
orientation=angle,
width=width,
layer=layer_wg,
)
c.auto_rename_ports()
return c
[docs]
@cell
def crossing45(
crossing: ComponentSpec = crossing,
port_spacing: float = 40.0,
dx: float | None = None,
alpha: float = 0.08,
npoints: int = 101,
cross_section: CrossSectionSpec = "xs_sc",
cross_section_bends: CrossSectionSpec = "xs_sc",
) -> Component:
r"""Returns 45deg crossing with bends.
Args:
crossing: crossing function.
port_spacing: target I/O port spacing.
dx: target length.
alpha: optimization parameter. diminish it for tight bends,
increase it if raises assertion angle errors
npoints: number of points.
cross_section: waveguide cross_section.
cross_section_bends: waveguide cross_section for the bends.
The 45 Degree crossing CANNOT be kept as an SRef since
we only allow for multiples of 90Deg rotations in SRef.
.. code::
---- ----
\ /
X
/ \
--- ----
"""
crossing = gf.get_component(
crossing, cross_section=cross_section_bends or cross_section
)
c = Component()
x = c << crossing
x.rotate(45)
# Add bends
p_e = x.ports["o3"].center
p_w = x.ports["o1"].center
p_n = x.ports["o2"].center
p_s = x.ports["o4"].center
# Flatten the crossing - not an SRef anymore
dx = dx or port_spacing
dy = port_spacing / 2
start_angle = 45
end_angle = 0
cpts = find_min_curv_bezier_control_points(
start_point=p_e,
end_point=(dx, dy),
start_angle=start_angle,
end_angle=end_angle,
npoints=npoints,
alpha=alpha,
)
bend = bezier(
control_points=cpts,
start_angle=start_angle,
end_angle=end_angle,
npoints=npoints,
cross_section=cross_section_bends,
)
tol = 1e-2
assert abs(bend.info["start_angle"] - start_angle) < tol, print(
f"{bend.info['start_angle']} differs from {start_angle}"
)
assert abs(bend.info["end_angle"] - end_angle) < tol, bend.info["end_angle"]
b_tr = bend.ref(position=p_e, port_id="o1")
b_tl = bend.ref(position=p_n, port_id="o1", h_mirror=True)
b_bl = bend.ref(position=p_w, port_id="o1", rotation=180)
b_br = bend.ref(position=p_s, port_id="o1", v_mirror=True)
for cmp_ref in [b_tr, b_br, b_tl, b_bl]:
# cmp_ref = _cmp.ref()
c.add(cmp_ref)
c.absorb(cmp_ref)
c.absorb(x)
c.info["bezier_length"] = bend.info["length"]
c.info["min_bend_radius"] = b_br.info["min_bend_radius"]
c.bezier = bend
c.crossing = crossing
c.add_port("o1", port=b_bl.ports["o2"])
c.add_port("o2", port=b_tl.ports["o2"])
c.add_port("o3", port=b_tr.ports["o2"])
c.add_port("o4", port=b_br.ports["o2"])
c.snap_ports_to_grid()
return c
crossing45_pins = partial(crossing45, cross_section="xs_sc")
[docs]
@cell
def compensation_path(
crossing45: ComponentSpec = crossing45_pins,
crossing: ComponentSpec = crossing,
direction: str = "top",
cross_section: CrossSectionSpec = "xs_sc",
) -> Component:
r"""Returns Component Path with same path length as the crossing.
with input and output ports having same y coordinates
Args:
crossing45: component that we want to match in path length.
needs to have .info["components"] with bends and crossing.
direction: the direction in which the bend should go "top" / "bottom".
.. code::
---- ----
\ /
\ /
\ /
X
/ \
/ \
/ \
---- ----
Compensation path:
.. code::
--+--
_/ \_
--/ \--
"""
import scipy.optimize as so
# Get total path length taken by the bends
crossing45 = gf.get_component(crossing45)
bezier_length = crossing45.info["bezier_length"]
length = 2 * bezier_length
# Find a bezier S-bend with half this length, but with a fixed length
# governed by the crossing45 X-distance (west to east ports) and
# the crossing x_distance
target_bend_length = length / 2
def get_x_span(cmp):
return cmp.ports["o3"].x - cmp.ports["o1"].x
x_span_crossing45 = get_x_span(crossing45)
x_span_crossing = get_x_span(crossing45.crossing)
# x span allowed for the bend
x0 = (x_span_crossing45 - x_span_crossing) / 2
def get_control_pts(x, y):
return [(0, 0), (x0 / 2, 0), (x0 / 2, y), (x0, y)]
def f(y):
control_points = get_control_pts(x0, y)
t = np.linspace(0, 1, 51)
path_points = bezier_curve(t, control_points)
return path_length(path_points) - target_bend_length
# the path length of the s-bend between two ports p0 and p1 is :
# - larger than the euclidean distance L2(p0, p1)
# - smaller than the manhattan distance DL(p0, p1)
#
# This gives the bounds for the brentq root finding
ya = target_bend_length - x0
yb = np.sqrt(target_bend_length**2 - x0**2)
solution = so.root_scalar(f, bracket=[ya, yb], method="brentq")
y_bend = solution.root
y_bend = snap_to_grid(y_bend)
v_mirror = direction != "top"
sbend = bezier(control_points=get_control_pts(x0, y_bend))
c = Component()
crossing0 = c << gf.get_component(crossing)
sbend_left = sbend.ref(
position=crossing0.ports["o1"], port_id="o2", v_mirror=v_mirror
)
sbend_right = sbend.ref(
position=crossing0.ports["o3"], port_id="o2", h_mirror=True, v_mirror=v_mirror
)
c.add(sbend_left)
c.add(sbend_right)
c.add_port("o1", port=sbend_left.ports["o1"])
c.add_port("o2", port=sbend_right.ports["o1"])
c.info["min_bend_radius"] = sbend.info["min_bend_radius"]
return c
def _demo() -> None:
"""Plot curvature of bends."""
from matplotlib import pyplot as plt
c = crossing45(port_spacing=20.0, dx=15)
c2 = compensation_path(crossing45=c)
print(c.info["min_bend_radius"])
print(c2.info["min_bend_radius"])
component = Component(name="top_lvl")
component.add(c.ref(port_id="o1"))
component.add(c2.ref(port_id="o1", position=(0, 10)))
bend_info1 = c.info["components"]["bezier_bend"].info
bend_info2 = c2.info["components"]["sbend"].info
DL = bend_info1["length"]
L2 = bend_info1["length"]
plt.plot(bend_info1["t"][1:-1] * DL, abs(bend_info1["curvature"]))
plt.plot(bend_info2["t"][1:-1] * L2, abs(bend_info2["curvature"]))
plt.xlabel("bend length (um)")
plt.ylabel("curvature (um^-1)")
component.show()
plt.show()
if __name__ == "__main__":
# c = crossing45()
c = compensation_path()
# c = crossing(
# cross_section=dict(
# cross_section="xs_sc",
# settings=dict(cladding_offsets=[0], cladding_layers=[(3, 0)]),
# )
# )
# print(c.ports["E1"].y - c.ports['o2'].y)
# print(c.get_ports_array())
# _demo()
# c = crossing_from_taper()
# c.pprint()
# c = crossing_etched()
# c = compensation_path()
# c = crossing45(port_spacing=40)
c.show(show_ports=False)