add_fan_in takes a set of N ports and produces N parallel waveguides that converge into a tightly-pitched bundle line — the input side of a bundle route. add_bundle_astar and [add_bundle_from_corners][doroutes.add_bundle_from_corners] call it internally on each end of a bundle, but you can also call it directly when you only need the converging stub (for example, to expose a bundle interface that a downstream router consumes, or to compress a fibre-array fan-out without a closed-loop A* pass).

There are three fan-in strategies, each with a different sweet spot:

  • "manhattan" (default) — a 90° comb. The most compact choice when port pitch is comfortable relative to the bend radius.
  • "sbend" — per-wire euler s-bend with a corner angle sized to the PDK radius for each wire's jog. The right choice when port pitch falls below 2·radius and manhattan can no longer fit two 90° corners between adjacent wires.
  • "lbend" — straight + 90° bend + straight per port, so the bundle exits perpendicular to the input direction. Useful when the downstream bundle needs to turn 90°, or when you need a tunable clearance before the first bend.

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

Imports

import doroutes as dr
import gdsfactory as gf
from cspdk.si220.cband import PDK

PDK.activate()

A 5-Port Frame

dr.pcells.fanout_frame(orientation="e") is an example layout with 5 east-facing ports spaced 40 µm apart inside a 100 µm-wide frame — typical of a fibre-array facet. We render the empty frame first as a reference, then apply each of the three fan-in strategies in turn on a fresh copy so you can compare the resulting geometries.

c = gf.Component()
ref = c << dr.pcells.fanout_frame(orientation="e")
c.add_ports(ref)
dr.util.show_cell(c)

Manhattan Fan-In (Default)

The classic 90°-comb fan-in. Each port goes straight, then turns 90° onto the bundle line. The bend lands at the port face — predictable and compact, but on tight pitches the bend body can intrude on neighbouring wires.

c = gf.Component()
ref = c << dr.pcells.fanout_frame(orientation="e")
c.add_ports(ref)
dr.add_fan_in(
    c=c,
    inputs=c.ports,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
)
dr.util.show_cell(c)

S-Bend Fan-In (Tight Pitch)

S-bend is the right choice when port pitch < 2·radius — the regime where the manhattan staircase can no longer fit two 90° corners without adjacent bend bodies overlapping. For looser pitches, manhattan is strictly more compact, so prefer it there.

The frame below uses a 7 µm port pitch with the same 5 µm bend radius (2·R = 10 µm > 7 µm pitch). Each port is routed via two euler bends connected by an angled straight: straight → bend_euler(θ) → straight → bend_euler(θ) → straight. The angle θ is computed per wire:

  • Wires with |jog| ≥ 2·radius use θ = 90° (full L-bend pair).
  • Wires with smaller jogs use the largest geometrically-feasible angle (θ = arccos(1 − |jog|/2R)) that keeps the corner at the PDK radius floor.
  • The centerline wire (zero jog) is a pure straight.

Adjacent wires are staggered so their diagonals stay spacing_dbu apart perpendicular to the bend, so outer wires never cross inner ones.

Tunable via strategy_kwargs: - max_sbend_angle_deg — uniform cap on the per-wire corner angle (default 90°). Lower values produce smoother bends across all wires at the cost of forward extent. - sbend_length_dbu — minimum forward extent (port → bundle line) in dbu. Useful when the downstream A* needs more clearance to bend the bundle.

c = gf.Component()
ref = c << dr.pcells.fanout_frame(
    orientation="e",
    num_inputs=5,
    input_spacing=7,  # < 2*radius = 10 µm — manhattan would not fit
    width=30,
)
c.add_ports(ref)
dr.add_fan_in(
    c=c,
    inputs=c.ports,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
    strategy="sbend",
)
dr.util.show_cell(c)
/home/runner/work/DoRoutes/DoRoutes/.venv/lib/python3.12/site-packages/cachetools/_cached.py:185: UserWarning: bend_euler angle should be 90 or 180. Got 67.04550059860719. Use bend_euler_all_angle instead.
  v = func(*args, **kwargs)

L-Bend Fan-In

Each port emits straight → 90° bend → straight, with the pre/post straight lengths chosen so all wires land on a common bundle line at spacing_dbu pitch. The bundle direction ends up perpendicular to the input direction.

Use this when the bundle needs to exit the fan-in turning 90° from the input ports — for example, when you're routing toward a destination on a perpendicular edge of the layout, or when you need a tunable clearance segment before the first bend (set via margin_dbu in strategy_kwargs).

When using add_fan_in directly you pass lbend_side ("left" or "right") explicitly. The full add_bundle_astar call sees both port sets at once and can pick the side automatically with lbend_side="auto".

c = gf.Component()
ref = c << dr.pcells.fanout_frame(orientation="e")
c.add_ports(ref)
dr.add_fan_in(
    c=c,
    inputs=c.ports,
    straight="straight",
    bend={"component": "bend_euler", "settings": {"radius": 5}},
    strategy="lbend",
    strategy_kwargs={"lbend_side": "left"},
)
dr.util.show_cell(c)

Next steps