This notebook shows how to wire arrayed instancesadd_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