[add_bundle_manual][doroutes.add_bundle_manual] is the bundle (multi-wire) counterpart of [add_route_manual][doroutes.add_route_manual]. It performs a fan-in at each end so the N wires converge onto a single bundle line, then routes that bundle through user-specified corners or steps.

Reach for it when:

  • the floorplan dictates an exact route shape (a routing channel, fence regions, hand-drawn paths),
  • the same routing recipe needs to apply to many similarly-shaped layouts (use steps= for placement-independent recipes), or
  • A* routing is overkill for a route you already know the shape of.

This is optical routing: bends are bend_euler at the PDK minimum radius (5 µm for gdsfactory.gpdk).

Imports

import gdsfactory as gf
from gdsfactory.gpdk import PDK

import doroutes as dr

PDK.activate()

A 5-Port Test Frame

dr.pcells.fanout_frame2(transition="ew") puts 5 east-facing inputs on the left edge and 5 west-facing outputs on the right edge of a 100 × 200 µm frame — the same layout used in the A* bundle routing tutorial, so you can compare the algorithmic and corner-based approaches side by side.

c = gf.Component()
ref = c << dr.pcells.fanout_frame2(transition="ew", add_frame=True)
c.add_ports(ref)
dr.util.show_cell(c)

Bundle Through Explicit Corners

Pass corners=[(x1, y1), (x2, y2), ...] to route the bundle through a sequence of absolute coordinate points (in microns).

The bundle's fan-in anchors at the port centroid by default — for this 5-port frame that's (1, 100) on the input side, so the bundle exits the input fan-in at roughly (13, 100) heading east. Each corner you list must be axis-aligned with the previous (manhattan routing), so corners describe a sequence of 90° turns from the bundle exit to the bundle stop on the other side.

The 4-corner Z-shape below detours the bundle through y=30: south at x=50, east at y=30, north at x=80 back to the output bundle line.

c = gf.Component()
ref = c << dr.pcells.fanout_frame2(transition="ew", add_frame=True)
c.add_ports(ref)

# Bundle exits at (~13, 100, east) and stops at (~87, 75, east).
# 4-corner Z: south, east, south, then back up to the output centroid y=75.
dr.add_bundle_manual(
    component=c,
    ports1=[p for p in c.ports if str(p.name).startswith("in")],
    ports2=[p for p in c.ports if str(p.name).startswith("out")],
    corners=[
        (50, 100),  # turn south at (50, 100) µm
        (50, 30),  # turn east at (50, 30)
        (80, 30),  # turn north at (80, 30)
        (80, 75),  # back to output centroid y=75; final east leg is implicit
    ],
    spacing=1.0,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
)
dr.util.show_cell(c)

Same Route via Relative Steps

[add_bundle_manual][doroutes.add_bundle_manual] with steps= is the relative-coordinate sibling. Each step describes a one-axis move from the previous corner (in microns). Steps anchor at the bundle exit (the post-fan-in convergence point), so step deltas describe the path starting from where the bundle leaves the fan-in. The four supported step forms are:

Key Meaning
dx move by Δx µm from the previous corner
dy move by Δy µm from the previous corner
x absolute x in µm (or "inst,port" to anchor to a port)
y absolute y in µm (or "inst,port" to anchor to a port)

The advantage of steps over corners: the recipe is independent of absolute placement, so the same step list works no matter where the parent component is instantiated.

c = gf.Component()
ref = c << dr.pcells.fanout_frame2(transition="ew", add_frame=True)
c.add_ports(ref)

# Same Z-shape detour, expressed as relative moves from the bundle exit
# (which sits at ~(13, 100) east of the input ports).
dr.add_bundle_manual(
    component=c,
    ports1=[p for p in c.ports if str(p.name).startswith("in")],
    ports2=[p for p in c.ports if str(p.name).startswith("out")],
    steps=[
        {"dy": -70},  # south 70 µm to y=30
        {"dx": 37},  # east 37 µm to x=50
        {"dy": 45},  # north 45 µm back to output centroid y=75
    ],
    spacing=1.0,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
)
dr.util.show_cell(c)

Choosing Between corners and steps

Both add_bundle_manual(corners=...) and add_bundle_manual(steps=...) call the same router under the hood; pick whichever feels natural for what you're trying to express:

  • corners — best when the route is anchored to a fixed floorplan (a routing channel, fence region, or specific coordinate constraint). You know the absolute (x, y) points the bundle has to pass through.
  • steps — best when the route shape is what matters and you want it to survive re-placement (the same recipe applies to many similar layouts).

Internally the bundle routes through a manhattan fan-in by default (add_fan_in(strategy="manhattan")). If you need a smoother fan-in (s-bend or l-bend), use add_bundle_astar with a fan_in= kwarg, or compose add_fan_in + [add_route_manual][doroutes.add_route_manual] directly.

Next steps

  • Don't want to spell the path out yourself? See A* Bundle Routing for the algorithmic alternative.
  • For the fan-in stage in isolation (and how to choose between manhattan, s-bend, and l-bend), see Fan-In.
  • For routing between arrayed instances (pad grids, probe cards), see Array Routing — it uses these same corner / step recipes on a real-world layout.