Skip to content

Download notebook (.ipynb)

Bundle Routing — Tutorial

route_bundle routes N start ports to N end ports, maintaining spacing and avoiding obstacles. This page covers parameters that are not shown in the Routing Overview or Optical Routing Deep Dive.

Topic API
Automatic port sorting sort_ports=True
Per-bundle separation separation= (DBU / µm)
S-bend compact routing sbend_factory=
Bounding-box strategy bbox_routing='minimal' / 'full'
Obstacle avoidance bboxes=[...], collision_check_layers=
Mismatch tolerance allow_width_mismatch, allow_type_mismatch

Setup

from functools import partial

import kfactory as kf


class LAYER(kf.LayerInfos):
    WG: kf.kdb.LayerInfo = kf.kdb.LayerInfo(1, 0)
    WGCLAD: kf.kdb.LayerInfo = kf.kdb.LayerInfo(2, 0)
    METAL: kf.kdb.LayerInfo = kf.kdb.LayerInfo(20, 0)


L = LAYER()
kf.kcl.infos = L

WG_WIDTH = kf.kcl.to_dbu(0.5)  # 500 DBU
SEP = kf.kcl.to_dbu(2.0)  # 2 µm centre-to-centre extra separation

wg_enc = kf.kcl.get_enclosure(
    kf.LayerEnclosure(name="WGSTD_BND", sections=[(L.WGCLAD, 0, 2_000)])
)

bend90 = kf.factories.euler.bend_euler_factory(kcl=kf.kcl)(
    width=0.5, radius=10, layer=L.WG, enclosure=wg_enc, angle=90
)
straight_factory = partial(
    kf.factories.straight.straight_dbu_factory(kcl=kf.kcl),
    layer=L.WG,
    enclosure=wg_enc,
)
bend_radius = kf.routing.optical.get_radius(bend90)

wl = kf.kcl.find_layer(L.WG)

1 · Port sorting — sort_ports=True

By default route_bundle pairs start_ports[i] with end_ports[i]. If the two lists are in opposite spatial orders (e.g., start ports run bottom-to-top while end ports run top-to-bottom), routes will cross each other.

Setting sort_ports=True re-orders both lists by position so that the port closest to the bottom on the start side connects to the port closest to the bottom on the end side — eliminating crossings.

# Start ports are ordered top-to-bottom; end ports are ordered bottom-to-top.
# Without sort_ports the routes would cross.


@kf.cell
def bundle_sorted() -> kf.KCell:
    c = kf.KCell()

    start_ports = [
        c.create_port(
            name=f"in{i}",
            trans=kf.kdb.Trans(2, False, -60_000, (2 - i) * 10_000),  # reversed Y
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        for i in range(3)
    ]
    end_ports = [
        c.create_port(
            name=f"out{i}",
            trans=kf.kdb.Trans(0, False, 60_000, i * 10_000),  # normal Y
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        for i in range(3)
    ]

    # Use a wider local separation matching the 10µm port pitch — the
    # global SEP=2µm would require the router to bend each route to
    # compress the bundle, which collides with adjacent route bends.
    kf.routing.optical.route_bundle(
        c,
        start_ports=start_ports,
        end_ports=end_ports,
        separation=kf.kcl.to_dbu(10),
        straight_factory=straight_factory,
        bend90_cell=bend90,
        sort_ports=True,  # ← automatically matches by Y position
    )
    return c


c_sorted = bundle_sorted()
c_sorted.plot()


png

Without sort_ports=True the same port lists would produce three crossing routes. Sorting resolves the crossing by pairing ports at the same relative position.

2 · Separation between routes

separation sets the minimum centre-to-centre distance between adjacent routes in the bundle. Increase it to open up more space between waveguides, for example near dense arrays where cladding layers would otherwise overlap.

@kf.cell
def bundle_wide_sep() -> kf.KCell:
    c = kf.KCell()
    N = 4
    for i in range(N):
        c.create_port(
            name=f"in{i}",
            trans=kf.kdb.Trans(2, False, -60_000, i * 4_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        c.create_port(
            name=f"out{i}",
            trans=kf.kdb.Trans(0, False, 60_000, i * 4_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )

    kf.routing.optical.route_bundle(
        c,
        start_ports=list(c.ports.filter(port_type="optical", regex="^in")),
        end_ports=list(c.ports.filter(port_type="optical", regex="^out")),
        separation=kf.kcl.to_dbu(5.0),  # 5 µm — wider than the default 2 µm
        straight_factory=straight_factory,
        bend90_cell=bend90,
        on_collision=None,
    )
    return c


bundle_wide_sep().plot()


png

3 · S-bend factory for compact fan-in / fan-out

When start and end ports are already colinear (same angle, facing each other), ordinary bend-based routing adds large out-of-plane detours. Passing an sbend_factory lets the router use S-bends instead — halving the vertical extent and producing a much more compact layout.

sbend_factory = kf.factories.euler.bend_s_euler_factory(kcl=kf.kcl)


@kf.cell
def bundle_sbend() -> kf.KCell:
    """Three parallel waveguides with an S-bend-optimised route."""
    c = kf.KCell()
    N = 3
    pitch = 3_000  # 3 µm pitch, same on both sides

    start_ports = [
        c.create_port(
            name=f"in{i}",
            trans=kf.kdb.Trans(2, False, -40_000, i * pitch),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        for i in range(N)
    ]
    end_ports = [
        c.create_port(
            name=f"out{i}",
            trans=kf.kdb.Trans(0, False, 40_000, i * pitch),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        for i in range(N)
    ]

    kf.routing.optical.route_bundle(
        c,
        start_ports=start_ports,
        end_ports=end_ports,
        separation=SEP,
        straight_factory=straight_factory,
        bend90_cell=bend90,
        sbend_factory=sbend_factory,  # ← enables S-bend optimisation
        on_collision=None,
    )
    return c


bundle_sbend().plot()


png

4 · Bounding-box routing strategy — bbox_routing

When obstacle boxes (bboxes=) are present the router must decide how far around them to detour.

Value Behaviour
'minimal' (default) Each route takes the shortest path around the obstacle.
'full' All routes share a single bounding path — the bundle stays intact as it detours.

'full' produces cleaner layouts when you want the bundle to remain grouped around obstacles; 'minimal' produces tighter total area when individual routes can fan around obstacles independently.

blocker = kf.kdb.Box(-8_000, 0, 8_000, 12_000)  # obstacle in DBU


@kf.cell
def bundle_bbox_minimal() -> kf.KCell:
    c = kf.KCell()
    N = 4
    for i in range(N):
        c.create_port(
            name=f"in{i}",
            trans=kf.kdb.Trans(2, False, -60_000, i * 3_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        c.create_port(
            name=f"out{i}",
            trans=kf.kdb.Trans(0, False, 60_000, i * 3_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )

    kf.routing.optical.route_bundle(
        c,
        start_ports=list(c.ports.filter(port_type="optical", regex="^in")),
        end_ports=list(c.ports.filter(port_type="optical", regex="^out")),
        separation=SEP,
        straight_factory=straight_factory,
        bend90_cell=bend90,
        bboxes=[blocker],
        bbox_routing="minimal",
        on_collision=None,
    )
    return c


@kf.cell
def bundle_bbox_full() -> kf.KCell:
    c = kf.KCell()
    N = 4
    for i in range(N):
        c.create_port(
            name=f"in{i}",
            trans=kf.kdb.Trans(2, False, -60_000, i * 3_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        c.create_port(
            name=f"out{i}",
            trans=kf.kdb.Trans(0, False, 60_000, i * 3_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )

    kf.routing.optical.route_bundle(
        c,
        start_ports=list(c.ports.filter(port_type="optical", regex="^in")),
        end_ports=list(c.ports.filter(port_type="optical", regex="^out")),
        separation=SEP,
        straight_factory=straight_factory,
        bend90_cell=bend90,
        bboxes=[blocker],
        bbox_routing="full",
        on_collision=None,
    )
    return c


bundle_bbox_minimal().plot()


png

bundle_bbox_full().plot()


png

5 · Collision-check layers

By default kfactory does not check for physical overlaps between routes and existing geometry. Pass collision_check_layers= to enable the check on specific layers. Use on_collision=None in scripts or notebooks to suppress the KLayout interactive error dialog; use on_collision='error' in CI to fail hard.

@kf.cell
def bundle_collision_check() -> kf.KCell:
    c = kf.KCell()
    N = 3
    for i in range(N):
        c.create_port(
            name=f"in{i}",
            trans=kf.kdb.Trans(2, False, -60_000, i * 3_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        c.create_port(
            name=f"out{i}",
            trans=kf.kdb.Trans(0, False, 60_000, i * 3_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )

    kf.routing.optical.route_bundle(
        c,
        start_ports=list(c.ports.filter(port_type="optical", regex="^in")),
        end_ports=list(c.ports.filter(port_type="optical", regex="^out")),
        separation=SEP,
        straight_factory=straight_factory,
        bend90_cell=bend90,
        collision_check_layers=[L.WG, L.WGCLAD],  # ← layers to check
        on_collision=None,  # suppress dialog in notebooks; use 'error' in CI
    )
    return c


bundle_collision_check().plot()


png

6 · Mismatch tolerance flags

route_bundle checks that paired ports share the same width, layer, and port type. Relax individual checks when connecting different structures:

Flag Effect
allow_width_mismatch=True Accept ports with different widths
allow_layer_mismatch=True Accept ports on different layers
allow_type_mismatch=True Accept ports with different port_type strings

These flags do not insert tapers — the route simply uses the width/layer of the first (start) port. Insert explicit taper_cell for mode-converting transitions.

@kf.cell
def bundle_type_mismatch() -> kf.KCell:
    """Connect 'optical' start ports to 'pin' end ports."""
    c = kf.KCell()
    N = 2

    start_ports = [
        c.create_port(
            name=f"wg{i}",
            trans=kf.kdb.Trans(2, False, -50_000, i * 4_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="optical",
        )
        for i in range(N)
    ]
    # End ports have a different port_type
    end_ports = [
        c.create_port(
            name=f"pin{i}",
            trans=kf.kdb.Trans(0, False, 50_000, i * 4_000),
            width=WG_WIDTH,
            layer=wl,
            port_type="pin",
        )
        for i in range(N)
    ]

    kf.routing.optical.route_bundle(
        c,
        start_ports=start_ports,
        end_ports=end_ports,
        separation=SEP,
        straight_factory=straight_factory,
        bend90_cell=bend90,
        allow_type_mismatch=True,  # ← suppress port_type equality check
        on_collision=None,
    )
    return c


bundle_type_mismatch().plot()


png

Parameter quick-reference

Parameter Type Default Notes
separation DBU / µm Min centre-to-centre spacing
sort_ports bool False Sort both lists by position before pairing
sbend_factory factory or None None Use S-bends for compact lateral shifts
bbox_routing 'minimal' / 'full' 'minimal' How the bundle detours around bboxes
bboxes list[kdb.Box] None Physical obstacles to route around
collision_check_layers list[LayerInfo] None Layers checked for geometric overlap
on_collision 'error' / 'show_error' / None 'show_error' Action on collision
allow_width_mismatch bool None Skip width equality check
allow_layer_mismatch bool None Skip layer equality check
allow_type_mismatch bool None Skip port_type equality check
route_width DBU / list[DBU] None Override wire width per route
starts / ends DBU / steps None Entry/exit stub lengths (see optical.py)
waypoints Trans / list[Point] None Force routes through a point (see optical.py)
path_length_matching_config PathLengthConfig None Equal-length routing (see optical.py)

See Also

Topic Where
Single-route optical options: waypoints, loopbacks, stubs Routing: Optical
Electrical bundle routing Routing: Electrical
Equal path-length loops inside a bundle Routing: Path Length
Manhattan backbone that bundle routing uses internally Routing: Manhattan
Routing overview and sub-module map Routing: Overview
Port sorting and orientation Core Concepts: Ports