Skip to content

A* Bundle Routing

add_bundle_astar routes N parallel waveguides as a single bundle between two port arrays, letting A* find a path that avoids obstacles while keeping the bundle intact. It ties three pieces together:

  1. a fan-in that converges the source ports onto a tight bundle line,
  2. an A* search that routes the bundle as a single thick wire while avoiding the listed obstacle layers,
  3. a fan-out that re-spreads the bundle onto the destination ports.

Reach for it when you have N input ports and N output ports, you want them routed together as a unit (matched bend and length), and the exact shape of the path doesn't matter as long as it avoids obstacles.

Two helper layouts in dr.pcells cover the two common cases:

  • fanout_frame2 — input and output sides face along the same axis (here transition="ew": inputs east-facing, outputs west-facing).
  • fanout_frame3 — input and output sides are rotated 90° relative to each other, so the bundle has to make a corner.

This is optical routing: bends are bend_euler at the PDK minimum radius (5 µm for gdsfactory.gpdk). The bundle's effective radius scales with the port count (single_radius + (N-1)*spacing/2), so wide bundles need more clearance to make a turn.

Imports

import gdsfactory as gf
from gdsfactory.gpdk import PDK

import doroutes as dr

PDK.activate()

In-Line Bundle with Obstacles

fanout_frame2(transition="ew") puts inputs on the west edge and outputs on the east edge of the frame. We drop two MMIs into the routing channel as obstacles — the A* path has to weave around them while keeping the bundle intact.

Pass clearance (µm) to leave a gap between the bundle body and every obstacle edge. With clearance=0 (default) the bundle can share an edge with an MMI face, which reads visually as overlap in klayout; here we use clearance=1.0.

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

# Drop two MMIs into the routing channel as obstacles.
r = c << gf.components.mmi()
r.dmove((40, 120))
r = c << gf.components.mmi()
r.drotate(90)
r.dmove((60, 40))

result = dr.add_bundle_astar(
    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")],
    spacing=1.0,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
    layers=["WG"],
    clearance=1.0,
)
dr.util.show_cell(c)
API key for organization 'GDSFactory' found.

Querying Per-Trace Route Lengths

add_bundle_astar returns a BundleResult whose .traces list contains one TraceRoute per wire. Each TraceRoute carries the routed corner polyline (.corners) and the geometric path length (.path_length, in µm) — port to port, including the fan-in/fan-out legs, with bend arcs counted as arcs — handy for loss budgets or length-matching checks across the bundle.

Use trace.port1_index and trace.port2_index to map each trace back to the corresponding entry in ports1 / ports2.

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")]
for trace in result.traces:
    p1 = ports1[trace.port1_index].name
    p2 = ports2[trace.port2_index].name
    print(f"Trace {p1}{p2}: {trace.path_length:.1f} µm")
Trace in3 → out3: 248.9 µm
Trace in1 → out1: 180.9 µm
Trace in0 → out0: 119.6 µm
Trace in2 → out2: 180.9 µm
Trace in4 → out4: 248.9 µm

Bundle Around a 90° Corner

fanout_frame3 rotates the output side by 90°, so the bundle's start and end orientations are perpendicular. The A* search has to turn the bundle through a corner — and the bundle's effective radius (set by port count and spacing) determines how much room that corner needs.

c = gf.Component()
ref = c << dr.pcells.fanout_frame3(transition="ew")
c.add_ports(ref)
dr.add_bundle_astar(
    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")],
    spacing=1.0,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
    layers=["WG"],
)
dr.util.show_cell(c)
API key for organization 'GDSFactory' found.

Next steps