"""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,
Port,
Ports,
)
from gdsfactory.utils import to_kdb_boxes
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 = "show_error",
bboxes: list[kf.kdb.Box] | None = None,
allow_width_mismatch: bool = False,
radius: float | None = None,
route_width: float | None = None,
straight: ComponentSpec = "straight",
auto_taper: bool = True,
waypoints: Coordinates | None = None,
steps: Sequence[Mapping[str, int | float]] | None = None,
start_angles: int | list[int] | None = None,
end_angles: int | list[int] | 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 the 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 show_error.
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.
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(f"top_{i}", center=(xs1[i], +0), width=0.5, orientation=a1, layer=(1,0)) for i in range(N)]
ports2 = [gf.Port(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 = [ports1] if isinstance(ports1, Port) else list(ports1)
ports2 = [ports2] if isinstance(ports2, Port) else 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")
xs = (
gf.get_cross_section(cross_section, width=route_width)
if route_width and not isinstance(route_width, list | tuple)
else gf.get_cross_section(cross_section)
)
width = route_width or xs.width
width_dbu = c.kcl.to_dbu(width)
radius = radius or xs.radius
taper_cell = gf.get_component(taper) if taper else None
end_straight = c.kcl.to_dbu(end_straight_length)
start_straight = c.kcl.to_dbu(start_straight_length)
if collision_check_layers:
collision_check_layer_enums = [
gf.get_layer(layer) for layer in collision_check_layers
]
else:
collision_check_layer_enums = None
if auto_taper:
ports1 = add_auto_tapers(component, ports1, cross_section)
ports2 = add_auto_tapers(component, ports2, cross_section)
if steps and waypoints:
raise ValueError("Cannot have both steps and waypoints")
if steps:
waypoints = []
x, y = ports1[0].dcenter
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.Point):
_waypoints: list[kf.kdb.Point] | None = [
c.kcl.to_dbu(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":
return kf.routing.electrical.route_bundle(
component,
ports1,
ports2,
c.kcl.to_dbu(separation),
starts=start_straight,
ends=end_straight,
collision_check_layers=collision_check_layer_enums, # type: ignore[arg-type]
on_collision=on_collision,
bboxes=to_kdb_boxes(bboxes or []),
route_width=width_dbu,
sort_ports=sort_ports,
waypoints=_waypoints,
end_angles=end_angles,
start_angles=start_angles,
)
bend90 = (
bend
if isinstance(bend, gf.Component)
else gf.get_component(
bend, cross_section=cross_section, radius=radius, width=width
)
)
def straight_dbu(width: int, length: int) -> gf.Component:
return gf.get_component(
straight,
length=c.kcl.to_um(length),
cross_section=xs,
)
return kf.routing.optical.route_bundle(
component,
ports1,
ports2,
c.kcl.to_dbu(separation),
straight_factory=straight_dbu,
bend90_cell=bend90,
taper_cell=taper_cell,
starts=start_straight,
ends=end_straight,
min_straight_taper=c.kcl.to_dbu(min_straight_taper),
place_port_type=port_type,
collision_check_layers=collision_check_layer_enums, # type: ignore[arg-type]
on_collision=on_collision,
allow_width_mismatch=allow_width_mismatch,
bboxes=to_kdb_boxes(bboxes or []),
route_width=width_dbu,
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.dmovex(300)
ptop.dmovey(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,
)
c.show()
# pbot.ports.print()
# c = gf.Component("demo")
# c1 = c << gf.components.mmi2x2()
# c2 = c << gf.components.mmi2x2()
# c2.dmove((100, 70))
# routes = route_bundle(
# c,
# [c1.ports["o2"], c1.ports["o1"]],
# [c2.ports["o2"], c2.ports["o1"]],
# separation=5,
# cross_section="strip",
# # 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.dmove((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(width=2, cross_section="rib")
# # c2 = c << gf.components.straight(cross_section="rib", width=1)
# c1 = c << gf.components.straight(cross_section="rib", width=2)
# c2 = c << gf.components.straight(cross_section="rib", width=4)
# c2.dmove((300, 70))
# 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,
# # taper=partial(gf.c.taper, cross_section="rib", length=20),
# # 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.dmove((100, 80))
# obstacle = gf.components.rectangle(size=(100, 10))
# obstacle1 = c << obstacle
# obstacle2 = c << obstacle
# obstacle1.dymin = 40
# obstacle2.dxmin = 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()