This notebook shows how to wire arrayed instances —
add_ref(rows=N, columns=M, ...) placements that show up whenever
you have grids of pads, probe arrays, or fan-outs. The routing APIs
are the same ones covered in the single-routing and bundle-routing
tutorials; the unique piece here is the port indexing that lets
you address individual elements of an array.
gdsfactory.Component.add_ref(columns=N, rows=M, ...) places a
component as a 2-D array. Ports on individual elements are addressed
with instance.ports[port_name, col, row] (kfactory's array port
indexing). Promoting those array ports onto the parent component
lets the standard bundle routers consume them as flat lists.
We use the same two-row pad layout to demonstrate three bundle routing approaches in turn — A*, corners, and steps — so you can see how to feed array-indexed ports into each.
Imports
from functools import partial
import doroutes as dr
import gdsfactory as gf
from gdsfactory.gpdk import get_generic_pdk
PDK = get_generic_pdk()
PDK.activate()
Two Rows of Pads
Two arrays of three pads, separated vertically by 500 µm. The top row
exposes its south-facing ports as e1..e3; the bottom row exposes its
north-facing ports as e4..e6. Promoting the array ports onto the
parent component lets the bundle routers consume them as flat lists.
def pad_arrays() -> gf.Component:
"""Two rows of 3 pads, 200 µm pitch, 500 µm vertical separation."""
c = gf.Component()
pad = gf.get_component("pad")
top = c.add_ref(pad, name="top", columns=3, rows=1, column_pitch=200, row_pitch=0)
top.dmove((0, 500))
bot = c.add_ref(pad, name="bot", columns=3, rows=1, column_pitch=200, row_pitch=0)
# Promote each array element's port onto the parent component using
# gdsfactory's array-port indexing: ports[name, col, row].
for i in range(3):
c.add_port(f"e{i + 1}", port=top.ports["e4", i, 0]) # top row, south face
c.add_port(f"e{i + 4}", port=bot.ports["e2", i, 0]) # bot row, north face
return c
A* Bundle Route
We let add_bundle_astar find a path that avoids the pad layer.
For how the algorithm picks routes (and how grid_unit and layers
control the search), see A* Bundle Routing.
Here we route on the metal-routing cross-section using wire_corner
for the bends — the natural choice for electrical traces, where bend
losses aren't a concern and a sharp corner is the most compact turn
possible.
c = pad_arrays()
ports1 = [p for p in c.ports if p.name in ["e1", "e2", "e3"]]
ports2 = [p for p in c.ports if p.name in ["e4", "e5", "e6"]]
dr.add_bundle_astar(
component=c,
ports1=ports1,
ports2=ports2,
spacing=20,
bend=partial(gf.get_component, "wire_corner", cross_section="metal_routing"),
straight=partial(gf.get_component, "straight", cross_section="metal_routing"),
layers=[(49, 0)], # pad metal layer — treated as an obstacle by A*
grid_unit=2500,
)
dr.util.show_cell(c)
API key for organization 'DoPlayDo' found.
Adding start/end stubs to clear adjacent pads
If you look closely at the result above, the wire_corner bend lands
right at each pad face. On a 200 µm pitch the corner body extends
sideways far enough to overlap the neighbouring pads — which would
short adjacent traces in a real layout. This is a common issue
whenever the bend body is comparable in size to the gap between
features.
The fix is to extend each pad outward with a short metal stub and promote the stub's far end as the routing port. The corner now lands clear of the array, with no overlap. This pattern (stub + promoted port) shows up often when wiring densely-packed bond pads.
def pad_arrays_with_stubs(stub_length: float = 50) -> gf.Component:
"""Same two pad rows, but with a short metal stub on each pad face.
The bundle now starts and ends at the stub tips, so the wire_corner
body lands clear of the pad array.
"""
c = gf.Component()
pad = gf.get_component("pad")
stub = gf.get_component(
"straight", length=stub_length, cross_section="metal_routing"
)
top = c.add_ref(pad, name="top", columns=3, rows=1, column_pitch=200, row_pitch=0)
top.dmove((0, 500))
bot = c.add_ref(pad, name="bot", columns=3, rows=1, column_pitch=200, row_pitch=0)
for i in range(3):
# The pad face is 100 µm wide and the metal_routing trace is 10 µm —
# connect with allow_width_mismatch=True since this junction is
# intentional (we're tapping into a pad).
s_top = c.add_ref(stub)
s_top.connect("e1", top.ports["e4", i, 0], allow_width_mismatch=True)
c.add_port(f"e{i + 1}", port=s_top.ports["e2"])
s_bot = c.add_ref(stub)
s_bot.connect("e1", bot.ports["e2", i, 0], allow_width_mismatch=True)
c.add_port(f"e{i + 4}", port=s_bot.ports["e2"])
return c
c = pad_arrays_with_stubs(stub_length=50)
ports1 = [p for p in c.ports if p.name in ["e1", "e2", "e3"]]
ports2 = [p for p in c.ports if p.name in ["e4", "e5", "e6"]]
dr.add_bundle_astar(
component=c,
ports1=ports1,
ports2=ports2,
spacing=20,
bend=partial(gf.get_component, "wire_corner", cross_section="metal_routing"),
straight=partial(gf.get_component, "straight", cross_section="metal_routing"),
layers=[(49, 0)],
grid_unit=2500,
)
dr.util.show_cell(c)
API key for organization 'DoPlayDo' found.
Corners Bundle Route
Same array layout, now using [add_bundle_from_corners][doroutes.add_bundle_from_corners] to specify the
exact path through two absolute corner points. For the full corners /
steps API and when to choose each, see
Bundle From Corners.
The two corners below produce a Z-shaped detour: south, east, south.
c = pad_arrays()
ports1 = [p for p in c.ports if p.name in ["e1", "e2", "e3"]]
ports2 = [p for p in c.ports if p.name in ["e4", "e5", "e6"]]
inv_dbu = int(1 / c.kcl.dbu) # µm → dbu multiplier (1000 with a 1 nm grid)
dr.add_bundle_from_corners(
component=c,
ports1=ports1,
ports2=ports2,
corners=[
(300 * inv_dbu, 250 * inv_dbu), # turn east at x=300 µm, y=250 µm
(150 * inv_dbu, 250 * inv_dbu), # turn south at x=150 µm, y=250 µm
],
spacing=20,
bend=partial(gf.c.wire_corner, cross_section="metal_routing"),
straight=partial(gf.c.straight, cross_section="metal_routing"),
)
dr.util.show_cell(c)
Steps Bundle Route
Same routing function, but the shape is given as relative steps
(dx/dy) anchored at the centroid of the start ports. Use this
form when you want a recipe that doesn't depend on absolute
coordinates — handy if you reuse the same routing pattern across
different placements.
The first step may be diagonal (both dx and dy) so the fan-in
starting point can be offset away from the source ports.
c = pad_arrays()
ports1 = [p for p in c.ports if p.name in ["e1", "e2", "e3"]]
ports2 = [p for p in c.ports if p.name in ["e4", "e5", "e6"]]
inv_dbu = int(1 / c.kcl.dbu)
dr.add_bundle_from_corners(
component=c,
ports1=ports1,
ports2=ports2,
steps=[
# diagonal first step: 100 µm east + 200 µm south of the ports1 centroid
{"dx": 100 * inv_dbu, "dy": -200 * inv_dbu},
{"dx": -150 * inv_dbu},
],
spacing=20,
bend=partial(gf.c.wire_corner, cross_section="metal_routing"),
straight=partial(gf.c.straight, cross_section="metal_routing"),
)
dr.util.show_cell(c)
Next steps
- For more bundle strategies (s-bend, l-bend) and a side-by-side comparison, see Bundle Routing Comparison.
- The single-wire versions of these routers are documented in Corners Routing and Steps Routing.
- Need obstacle avoidance without specifying the shape? See A* Bundle Routing.