Source code for gdsfactory.routing.route_bundle

"""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()