This tutorial walks through the bundle-routing methods you have available, on a single shared topology so the differences are visible at the bundle merge: 5 metal pads on top (200 µm pitch), 5 metal pads on bottom (150 µm pitch). The mismatched pitches mean the routes have to converge between the two arrays.

The notebook covers, in order:

  1. gdsfactory.routing.route_bundle — gdsfactory's built-in router, as a baseline.
  2. DoRoutes per-port A* — N independent A* routes, no fan-in.
  3. add_bundle_astar (manhattan fan-in) — the default DoRoutes bundle strategy.
  4. add_bundle_astar (s-bend fan-in) — per-wire euler s-bend.
  5. add_bundle_astar (l-bend fan-in) — per-port 90° turn so the bundle wraps around the layout and meets head-to-head from the opposite side.

Use this as a menu: pick the section that matches what your design needs (compactness, obstacle avoidance, tight pitches, orthogonal turns), then see the dedicated tutorials for each method for the underlying mechanics.

This is electrical / metal routing, so we use wire_corner (square 90° corners) on the metal_routing cross-section throughout. The L-bend section is the one exception — the U-shaped bundle bend it produces needs a finite-radius bend, so that section uses bend_euler instead (curved metal trace).

Setup

from functools import partial

import gdsfactory as gf
from gdsfactory.gpdk import get_generic_pdk

import doroutes as dr
from doroutes.routing import add_route_astar

PDK = get_generic_pdk()
PDK.activate()

NUM_PORTS = 5
SRC_PITCH = 200  # µm between source pads
DST_PITCH = 150  # µm between destination pads
VSEP = 1000  # µm vertical separation between top and bot pad rows
SPACING = 20

BEND_SPEC = partial(gf.get_component, "wire_corner", cross_section="metal_routing")
STRAIGHT_SPEC = partial(gf.get_component, "straight", cross_section="metal_routing")
LAYERS_TO_AVOID = [(49, 0)]
GRID_UNIT = 2500


def make_topology(name, n=NUM_PORTS, src_pitch=SRC_PITCH, dst_pitch=DST_PITCH):
    """Create a component with n source pads and n destination pads."""
    c = gf.Component(name)
    pad = gf.get_component("pad")
    top = c.add_ref(
        pad, name="top", columns=n, rows=1, column_pitch=src_pitch, row_pitch=0
    )
    top.dmove((0, VSEP))
    bot = c.add_ref(
        pad, name="bot", columns=n, rows=1, column_pitch=dst_pitch, row_pitch=0
    )
    p1 = [top.ports["e4", i, 0] for i in range(n)]
    p2 = [bot.ports["e2", i, 0] for i in range(n)]
    return c, p1, p2

1. Standard gdsfactory.routing.route_bundle

The baseline: gdsfactory's built-in bundle router.

c, p1, p2 = make_topology("gf_route_bundle")
xs = gf.get_cross_section("metal_routing")
gf.routing.route_bundle(
    c,
    ports1=p1,
    ports2=p2,
    cross_section=xs,
    bend="wire_corner",
    sort_ports=True,
)
c.show()
c
/home/runner/work/DoRoutes/DoRoutes/.venv/lib/python3.12/site-packages/gdsfactory/components/waveguides/wire.py:43: UserWarning: {'width': 10.0} ignored for cross_section 'metal_routing'
  x = gf.get_cross_section(cross_section, width=width)
/home/runner/work/DoRoutes/DoRoutes/.venv/lib/python3.12/site-packages/gdsfactory/components/waveguides/straight.py:33: UserWarning: {'width': 10.0} ignored for cross_section 'metal_routing'
  x = gf.get_cross_section(cross_section, width=width)


2026-04-30 06:17:33.438 | WARNING  | kfactory.kcell:show:3981 - Could not connect to klive server

png

2. DoRoutes A* without fan-in (per port)

Each port pair is routed independently using A*. No fan-in/fan-out is performed, so each waveguide finds its own obstacle-avoiding path.

c, p1, p2 = make_topology("astar_per_port")
ASTAR_BEND = partial(gf.get_component, "bend_euler", cross_section="metal_routing")

try:
    for src, dst in zip(p1, p2, strict=True):
        add_route_astar(
            c=c,
            start=src,
            stop=dst,
            straight=STRAIGHT_SPEC,
            bend=ASTAR_BEND,
            layers=LAYERS_TO_AVOID,
            grid_unit=GRID_UNIT,
        )
except RuntimeError:
    print("A* per-port routing failed on this topology (known limitation)")
c.show()
c
API key for organization 'DoPlayDo' found.


API key for organization 'DoPlayDo' found.
API key for organization 'DoPlayDo' found.


API key for organization 'DoPlayDo' found.


API key for organization 'DoPlayDo' found.
A* per-port routing failed on this topology (known limitation)
2026-04-30 06:17:38.787 | WARNING  | kfactory.kcell:show:3981 - Could not connect to klive server

png

3. DoRoutes add_bundle_astar – Manhattan fan-in

The default bundle A* strategy. Each port first turns 90° onto a common bundle line via right-angle steps (the "manhattan staircase"), and then a single A* path is found for the whole bundle.

This is the most compact choice when your port pitch is comfortable relative to the bend radius (specifically, when port_pitch ≥ 2·radius so two adjacent corners can fit side-by-side). Both odd and even port counts are supported, though even counts are slightly more sensitive to fan-in geometry near the bundle center.

c, p1, p2 = make_topology("bundle_manhattan_5")
dr.add_bundle_astar(
    component=c,
    ports1=p1,
    ports2=p2,
    spacing=SPACING,
    straight=STRAIGHT_SPEC,
    bend=BEND_SPEC,
    layers=LAYERS_TO_AVOID,
    grid_unit=GRID_UNIT,
)
c.show()
c
API key for organization 'DoPlayDo' found.
2026-04-30 06:17:39.992 | WARNING  | kfactory.kcell:show:3981 - Could not connect to klive server

png

Even port count

With an even number of ports there's no centerline wire, so the fan-in centroid lands between two ports. Manhattan handles this by shifting the bundle stops by half a port pitch. On most layouts this is invisible; tight pitches near the 2·radius limit can occasionally need a different strategy (see s-bend, below).

c, p1, p2 = make_topology("bundle_manhattan_6", n=6)
dr.add_bundle_astar(
    component=c,
    ports1=p1,
    ports2=p2,
    spacing=SPACING,
    straight=STRAIGHT_SPEC,
    bend=BEND_SPEC,
    layers=LAYERS_TO_AVOID,
    grid_unit=GRID_UNIT,
)
c.show()
c
API key for organization 'DoPlayDo' found.
2026-04-30 06:17:41.578 | WARNING  | kfactory.kcell:show:3981 - Could not connect to klive server

png

4. DoRoutes add_bundle_astar – S-bend fan-in

Same call as section 3 with fanin_strategy="sbend". Each wire is routed via two euler bends connected by an angled straight, with the corner angle sized per wire to the PDK radius. For the geometry and the max_sbend_angle_deg / sbend_length_dbu knobs, see Fan-In.

On this topology (200 / 150 µm pitches, 5 µm bend radius — both pitches comfortably above 2·radius = 10 µm), every wire ends up at 90° and the result is essentially identical to the manhattan staircase. S-bend's actual benefit appears when port pitch falls below 2·radius — the regime where manhattan can no longer fit two 90° corners between adjacent wires.

c, p1, p2 = make_topology("bundle_sbend_5")
dr.add_bundle_astar(
    component=c,
    ports1=p1,
    ports2=p2,
    spacing=SPACING,
    straight=STRAIGHT_SPEC,
    bend=BEND_SPEC,
    layers=LAYERS_TO_AVOID,
    grid_unit=GRID_UNIT,
    fanin_strategy="sbend",
)
c.show()
c
API key for organization 'DoPlayDo' found.
2026-04-30 06:17:42.583 | WARNING  | kfactory.kcell:show:3981 - Could not connect to klive server

png

5. DoRoutes add_bundle_astar – L-bend fan-in

Same call with fanin_strategy="lbend". Each port turns 90° through a single euler bend, sending the bundle perpendicular to the input direction. On the parallel pad topology this gives a U-shaped route: the top bundle exits west, the bottom bundle exits east, and A* wraps the two bundles around the layout so they meet head-to-head from the opposite side.

Reach for L-bend when source and destination port arrays sit on the same axis without a horizontal offset, and routing straight between them isn't an option (e.g. there's an obstacle in between).

This section uses bend_euler rather than wire_corner: the U-shape needs a finite-radius bend in the bundle's turn. If your design tolerates curved metal traces, this is the trade-off — sharp 90° corners across the whole bundle aren't possible for this geometry.

EULER_BEND_SPEC = partial(gf.get_component, "bend_euler", cross_section="metal_routing")

c, p1, p2 = make_topology("bundle_lbend_5")
dr.add_bundle_astar(
    component=c,
    ports1=p1,
    ports2=p2,
    spacing=SPACING,
    straight=STRAIGHT_SPEC,
    bend=EULER_BEND_SPEC,  # finite-radius bend for the U-shaped bundle turn
    layers=LAYERS_TO_AVOID,
    grid_unit=GRID_UNIT,
    fanin_strategy="lbend",
)
c.show()
c
API key for organization 'DoPlayDo' found.
2026-04-30 06:17:46.595 | WARNING  | kfactory.kcell:show:3981 - Could not connect to klive server

png

Next steps

  • For a deeper look at the fan-in stage and tight-pitch behaviour, see Fan-In.
  • For the underlying single-bundle A* call, see A* Bundle Routing.
  • For routing between arrayed instances (pad grids, probe cards), see Array Routing.