"""Routes bundles of ports (river routing).
get bundle is the generic river routing function
route_bundle calls different function depending on the port orientation.
- route_bundle_same_axis: ports facing each other with arbitrary pitch on each side
- route_bundle_corner: 90Deg / 270Deg between ports with arbitrary pitch
- route_bundle_udirect: ports with direct U-turns
- route_bundle_uindirect: ports with indirect U-turns
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from functools import partial
from typing import Literal
import kfactory as kf
from kfactory.routing.generic import ManhattanRoute
import gdsfactory as gf
from gdsfactory.routing.auto_taper import add_auto_tapers
from gdsfactory.routing.sort_ports import get_port_x, get_port_y
from gdsfactory.typings import (
STEP_DIRECTIVES,
ComponentSpec,
Coordinates,
CrossSectionSpec,
LayerSpec,
LayerSpecs,
Ports,
)
OpticalManhattanRoute = ManhattanRoute
TOLERANCE = 1
def get_min_spacing(
ports1: Ports,
ports2: Ports,
separation: float = 5.0,
radius: float = 5.0,
sort_ports: bool = True,
) -> float:
"""Returns the minimum amount of spacing in um required to create a fanout.
Args:
ports1: first list of ports.
ports2: second list of ports.
separation: minimum separation between two straights in um.
radius: bend radius in um.
sort_ports: sort the ports according to the axis.
"""
axis = "X" if ports1[0].orientation in [0, 180] else "Y"
j = 0
min_j = 0
max_j = 0
if sort_ports:
if axis in {"X", "x"}:
sorted(ports1, key=get_port_y)
sorted(ports2, key=get_port_y)
else:
sorted(ports1, key=get_port_x)
sorted(ports2, key=get_port_x)
for port1, port2 in zip(ports1, ports2):
if axis in {"X", "x"}:
x1 = get_port_y(port1)
x2 = get_port_y(port2)
else:
x1 = get_port_x(port1)
x2 = get_port_x(port2)
if x2 >= x1:
j += 1
else:
j -= 1
if j < min_j:
min_j = j
if j > max_j:
max_j = j
j = 0
return (max_j - min_j) * separation + 2 * radius + 1.0
[docs]
def route_bundle(
component: gf.Component,
ports1: Ports,
ports2: Ports,
cross_section: CrossSectionSpec | None = None,
layer: LayerSpec | None = None,
separation: float = 3.0,
bend: ComponentSpec = "bend_euler",
sort_ports: bool = False,
start_straight_length: float = 0,
end_straight_length: float = 0,
min_straight_taper: float = 100,
taper: ComponentSpec | None = None,
port_type: str | None = None,
collision_check_layers: LayerSpecs | None = None,
on_collision: Literal["error", "show_error"] | None = None,
bboxes: Sequence[kf.kdb.DBox] | None = None,
allow_width_mismatch: bool = False,
radius: float | None = None,
route_width: float | None = None,
straight: ComponentSpec = "straight",
auto_taper: bool = True,
auto_taper_taper: ComponentSpec | None = None,
waypoints: Coordinates | None = None,
steps: Sequence[Mapping[str, int | float]] | None = None,
start_angles: float | list[float] | None = None,
end_angles: float | list[float] | None = None,
router: Literal["optical", "electrical"] | None = None,
) -> list[ManhattanRoute]:
"""Places a bundle of routes to connect two groups of ports.
Routes connect a bundle of ports with a river router.
Chooses the correct routing function depending on port angles.
Args:
component: component to add the routes to.
ports1: list of starting ports.
ports2: list of end ports.
cross_section: CrossSection or function that returns a cross_section.
layer: layer to use for the route.
separation: bundle separation (center to center). Defaults to cross_section.width + cross_section.gap
bend: function for the bend. Defaults to euler.
sort_ports: sort port coordinates.
start_straight_length: straight length at the beginning of the route. If None, uses default value for the routing CrossSection.
end_straight_length: end length at the beginning of the route. If None, uses default value for the routing CrossSection.
min_straight_taper: minimum length for tapering the straight sections.
taper: function for tapering long straight waveguides beyond min_straight_taper. Defaults to None.
port_type: type of port to place. Defaults to optical.
collision_check_layers: list of layers to check for collisions.
on_collision: action to take on collision. Defaults to None (ignore).
bboxes: list of bounding boxes to avoid collisions.
allow_width_mismatch: allow different port widths.
radius: bend radius. If None, defaults to cross_section.radius.
route_width: width of the route. If None, defaults to cross_section.width.
straight: function for the straight. Defaults to straight.
auto_taper: if True, auto-tapers ports to the cross-section of the route.
auto_taper_taper: taper to use for auto-tapering. If None, uses the default taper for the cross-section.
waypoints: list of waypoints to add to the route.
steps: list of steps to add to the route.
start_angles: list of start angles for the routes. Only used for electrical ports.
end_angles: list of end angles for the routes. Only used for electrical ports.
router: Set the type of router to use, either the optical one or the electrical one.
If None, the router is optical unless the port_type is "electrical".
.. plot::
:include-source:
import gdsfactory as gf
dy = 200.0
xs1 = [-500, -300, -100, -90, -80, -55, -35, 200, 210, 240, 500, 650]
pitch = 10.0
N = len(xs1)
xs2 = [-20 + i * pitch for i in range(N // 2)]
xs2 += [400 + i * pitch for i in range(N // 2)]
a1 = 90
a2 = a1 + 180
ports1 = [gf.Port(name=f"top_{i}", center=(xs1[i], +0), width=0.5, orientation=a1, layer=(1,0)) for i in range(N)]
ports2 = [gf.Port(name=f"bot_{i}", center=(xs2[i], dy), width=0.5, orientation=a2, layer=(1,0)) for i in range(N)]
c = gf.Component()
gf.routing.route_bundle(component=c, ports1=ports1, ports2=ports2, cross_section='strip', separation=5)
c.plot()
"""
if layer and cross_section:
raise ValueError(
f"Cannot have both {layer=} and {cross_section=} provided. Choose one."
)
if cross_section is None:
if layer is not None and route_width is not None:
cross_section = partial(
gf.cross_section.cross_section, layer=layer, width=route_width
)
else:
raise ValueError(
f"Either {cross_section=} or {layer=} and {route_width=} must be provided"
)
c = component
ports1_ = list(ports1)
ports2_ = list(ports2)
port_type = port_type or ports1_[0].port_type
if len(ports1_) != len(ports2_):
raise ValueError(
f"ports1={len(ports1_)} and ports2={len(ports2_)} must be equal"
)
if route_width is None or route_width == 0:
xs = gf.get_cross_section(cross_section)
else:
xs = gf.get_cross_section(cross_section, width=route_width)
width = route_width or xs.width
radius = radius or xs.radius
taper_cell = gf.get_component(taper) if taper else None
if collision_check_layers:
collision_check_layer_enums = [
gf.get_layer(layer) for layer in collision_check_layers
]
else:
collision_check_layer_enums = None
bboxes = list(bboxes or [])
if auto_taper and auto_taper_taper:
taper_ = gf.get_component(auto_taper_taper)
taper_o1 = taper_.ports[0].name
taper_o2 = taper_.ports[1].name
ports1_new = []
ports2_new = []
for p1, p2 in zip(ports1_, ports2_):
t1 = c << taper_
t2 = c << taper_
t1.connect(taper_o1, p1)
t2.connect(taper_o1, p2)
ports1_new.append(t1.ports[taper_o2])
ports2_new.append(t2.ports[taper_o2])
ports1_ = ports1_new
ports2_ = ports2_new
bbox1 = gf.kdb.DBox()
bbox2 = gf.kdb.DBox()
for port in ports1_:
bbox1 += port.dcplx_trans.disp.to_p()
for port in ports2_:
bbox2 += port.dcplx_trans.disp.to_p()
bboxes.append(bbox1)
bboxes.append(bbox2)
elif auto_taper:
bbox1 = gf.kdb.DBox()
bbox2 = gf.kdb.DBox()
for port in ports1_:
bbox1 += port.dcplx_trans.disp.to_p()
for port in ports2_:
bbox2 += port.dcplx_trans.disp.to_p()
ports1_ = add_auto_tapers(component, ports1_, cross_section)
ports2_ = add_auto_tapers(component, ports2_, cross_section)
for port in ports1_:
bbox1 += port.dcplx_trans.disp.to_p()
for port in ports2_:
bbox2 += port.dcplx_trans.disp.to_p()
bboxes.append(bbox1)
bboxes.append(bbox2)
# component.shapes(component.kcl.layer(1,0)).insert(bbox)
if steps and waypoints:
raise ValueError("Cannot have both steps and waypoints")
if steps:
waypoints = []
x, y = ports1_[0].center
for d in steps:
if not STEP_DIRECTIVES.issuperset(d):
invalid_step_directives = list(set(d.keys()) - STEP_DIRECTIVES)
raise ValueError(
f"Invalid step directives: {invalid_step_directives}."
f"Valid directives are {list(STEP_DIRECTIVES)}"
)
x = d.get("x", x) + d.get("dx", 0)
y = d.get("y", y) + d.get("dy", 0)
waypoints += [(x, y)]
if waypoints is not None and not isinstance(waypoints[0], kf.kdb.DPoint):
waypoints_: list[kf.kdb.DPoint] | None = [
kf.kdb.DPoint(p[0], p[1]) for p in waypoints
]
else:
waypoints_ = waypoints
router = router or "electrical" if port_type == "electrical" else "optical"
if router == "electrical":
if cross_section is not None:
xs = gf.get_cross_section(cross_section)
layer_: gf.kdb.LayerInfo | None = gf.kcl.get_info(
gf.get_layer(xs.sections[0].layer)
)
else:
layer_ = None
return kf.routing.electrical.route_bundle(
component,
ports1_,
ports2_,
separation=separation,
starts=start_straight_length,
ends=end_straight_length,
collision_check_layers=[
c.kcl.layout.get_info(layer) for layer in collision_check_layer_enums
]
if collision_check_layer_enums is not None
else None,
on_collision=on_collision,
bboxes=bboxes,
route_width=width,
sort_ports=sort_ports,
waypoints=waypoints_,
end_angles=end_angles,
start_angles=start_angles,
place_layer=layer_,
)
bend90 = (
bend
if isinstance(bend, gf.Component)
else gf.get_component(
bend, cross_section=cross_section, radius=radius, width=width
)
)
def straight_um(width: float, length: float) -> gf.Component:
return gf.get_component(
straight, length=length, cross_section=cross_section, width=width
)
return kf.routing.optical.route_bundle(
component,
ports1_,
ports2_,
separation=separation,
straight_factory=straight_um,
bend90_cell=bend90,
taper_cell=taper_cell,
starts=start_straight_length,
ends=end_straight_length,
min_straight_taper=min_straight_taper,
place_port_type=port_type,
collision_check_layers=[
c.kcl.layout.get_info(layer) for layer in collision_check_layer_enums
]
if collision_check_layer_enums
else None,
on_collision=on_collision,
allow_width_mismatch=allow_width_mismatch,
bboxes=list(bboxes or []),
route_width=width,
sort_ports=sort_ports,
waypoints=waypoints_,
end_angles=end_angles,
start_angles=start_angles,
)
route_bundle_electrical = partial(
route_bundle,
bend="wire_corner",
allow_width_mismatch=True,
)
if __name__ == "__main__":
import gdsfactory as gf
from gdsfactory.generic_tech import LAYER
pdk = gf.get_active_pdk()
pdk.layer_transitions[LAYER.WG] = partial(
gf.c.taper, cross_section="rib", length=20
)
pdk.layer_transitions[LAYER.WG, LAYER.WGN] = gf.c.taper_sc_nc
pdk.layer_transitions[LAYER.WGN, LAYER.WG] = gf.c.taper_nc_sc
# c = gf.Component()
# columns = 2
# ptop = c << gf.components.pad_array(columns=columns, port_orientation=270)
# pbot = c << gf.components.pad_array(port_orientation=270, columns=columns)
# # pbot = c << gf.components.pad_array(port_orientation=90, columns=columns)
# ptop.movex(300)
# ptop.movey(300)
# routes = gf.routing.route_bundle_electrical(
# c,
# list(reversed(pbot.ports)),
# ptop.ports,
# # end_straight_length=50,
# start_straight_length=100,
# separation=20,
# bboxes=[ptop.bbox(), pbot.bbox()],
# cross_section="metal_routing",
# start_angles=None,
# end_angles=None,
# route_width=2,
# steps=[
# {"dy": 1, "dx": 1},
# {"dy": 2, "dx": 1},
# ],
# )
# c.show()
# pbot.ports.print()
c = gf.Component(name="demo")
c1 = c << gf.components.mmi2x2()
c2 = c << gf.components.mmi2x2()
c2.move((100, 70))
routes = route_bundle(
c,
[c1.ports["o2"], c1.ports["o1"]],
[c2.ports["o2"], c2.ports["o1"]],
separation=5,
cross_section="strip",
sort_ports=True,
# end_straight_length=0,
# collision_check_layers=[(1, 0)],
# bboxes=[c1.bbox(), c2.bbox()],
# layer=(2, 0),
# straight=partial(gf.components.straight, layer=(2, 0), width=1),
)
c.show()
# dy = 200.0
# xs1 = [-500, -300, -100, -90, -80, -55, -35, 200, 210, 240, 500, 650]
# pitch = 10.0
# N = len(xs1)
# xs2 = [-20 + i * pitch for i in range(N // 2)]
# xs2 += [400 + i * pitch for i in range(N // 2)]
# a1 = 90
# a2 = a1 + 180
# ports1 = [
# gf.Port(
# f"bot_{i}", center=(xs1[i], +0), width=0.5, orientation=a1, layer=(1, 0)
# )
# for i in range(N)
# ]
# ports2 = [
# gf.Port(
# f"top_{i}", center=(xs2[i], dy), width=0.5, orientation=a2, layer=(1, 0)
# )
# for i in range(N)
# ]
# c = gf.Component()
# route_bundle(
# c,
# ports1,
# ports2,
# end_straight_length=1,
# start_straight_length=100,
# )
# c.add_ports(ports1)
# c.add_ports(ports2)
# c.show()
# nitride case
# c = gf.Component()
# c1 = c << gf.components.straight(width=0.5, cross_section="strip")
# c2 = c << gf.components.straight(cross_section="strip", width=0.5)
# c2.move((150, 50))
# routes = route_bundle(
# c,
# [c1.ports["o2"]],
# [c2.ports["o1"]],
# separation=5,
# cross_section="nitride",
# auto_taper=True,
# )
# c.show()
# rib
c = gf.Component()
c1 = c << gf.components.straight(cross_section="rib", width=2)
c2 = c << gf.components.straight(cross_section="rib", width=2)
c2.move((300, 90))
routes = route_bundle(
c,
[c1.ports["o2"]],
[c2.ports["o1"]],
# waypoints=[(200, 40), (200, 50)],
# steps=[dict(dx=50, dy=100)],
# steps=[dict(dx=50, dy=100), dict(dy=100)],
separation=5,
cross_section="rib",
# auto_taper=True,
auto_taper_taper=partial(
gf.c.taper, cross_section="rib", length=10, width1=2, width2=1
),
route_width=1,
# auto_taper=False,
# taper=gf.c.taper_sc_nc,
# taper=gf.c.taper,
)
c.show()
# c = gf.Component()
# w = gf.components.array(gf.c.straight, columns=1, rows=3, spacing=(3, 3))
# left = c << w
# right = c << w
# right.move((100, 80))
# obstacle = gf.components.rectangle(size=(100, 10))
# obstacle1 = c << obstacle
# obstacle2 = c << obstacle
# obstacle1.ymin = 40
# obstacle2.xmin = 35
# ports1 = left.ports.filter(orientation=0)
# ports2 = right.ports.filter(orientation=180)
# routes = gf.routing.route_bundle(
# c,
# ports1,
# ports2,
# steps=[
# {"dy": 30, "dx": 50},
# {"dy": 30, "dx": 50},
# # {"dx": 90},
# ],
# cross_section="strip",
# # layer=(2, 0),
# route_width=0.2,
# )
# c.show()