Skip to content

Automatic Placement

DoRoutes places component rectangles with true pin offsets, coherent device groups, optional fence regions, and opt-in orientation optimization. The engine uses integer database units (DBU) internally and does not require a PDK.

place_auto reads and updates kfactory instances. For callers that keep their geometry elsewhere, place_components_dbu exposes the same Rust engine using plain dictionaries and returns moves for the caller to apply.

Build a PDK-free kfactory fixture

import kfactory as kf

import doroutes as dr

kcl = kf.KCLayout("placement_notebook")
layer = kcl.layer(1, 0)


def device(name: str) -> kf.KCell:
    """Return a small device with an off-center pin."""
    cell = kcl.kcell(name)
    cell.shapes(layer).insert(kf.kdb.DBox(-3.0, -4.0, 3.0, 4.0))
    cell.create_port(
        name="d",
        trans=kf.kdb.Trans(0, False, 1_500, 4_000),
        width=300,
        layer=layer,
    )
    return cell


top = kcl.kcell("placement_top")
names = ["left_0", "left_1", "right_0", "right_1"]
for name in names:
    inst = top.create_inst(device(name))
    inst.name = name

Groups, fences, and pin-true orientation

The two groups are constrained to separate fence regions. Pin offsets make the wirelength objective orientation-aware; when place_auto chooses a mirror, it applies that mirror before re-centering the instance at the reported location.

Auto-orientation is off by default (auto_orientation="none"), so a layout's hand-chosen orientations are never rewritten behind your back. Set auto_orientation="r180" to let the placer pick from R0, R180, MX and MY — a 180-degree rotation and the two mirror flips, all of which preserve the instance footprint. There is no setting that rotates a device sideways.

nets = [
    {
        "name": "signal_0",
        "pins": [
            {"inst": "left_0", "offset_dbu": (1_500, 4_000)},
            {"inst": "right_0", "offset_dbu": (1_500, -4_000)},
        ],
    },
    {
        "name": "signal_1",
        "pins": [
            {"inst": "left_1", "offset_dbu": (1_500, 4_000)},
            {"inst": "right_1", "offset_dbu": (1_500, -4_000)},
        ],
    },
]

report = dr.place_auto(
    top,
    instances=[
        {
            "name": name,
            "group": "left_pair" if name.startswith("left") else "right_pair",
            "fence": "left" if name.startswith("left") else "right",
        }
        for name in names
    ],
    nets=nets,
    mode="tiling",
    objective={"grouping_weight": 1.0, "auto_orientation": "r180"},
    constraints={
        "min_spacing": 1_000,
        "bbox": (30_000, 50_000, 0, 0),
        "fence_regions": [
            {"name": "left", "bbox": (0, 0, 22_000, 30_000)},
            {"name": "right", "bbox": (28_000, 0, 50_000, 30_000)},
        ],
    },
    iterations=40,
)

assert report["legalized"], report["placement_violations"]
report["placements"]
{'left_0': (12000, 10489, 'R0'),
 'left_1': (19000, 10489, 'R0'),
 'right_0': (31000, 19490, 'MY'),
 'right_1': (38000, 19490, 'MY')}

Every reported orientation is reflected in the live port geometry. R0 keeps the device pin at +1.5 µm from the instance center; MY moves it to -1.5 µm. With the default auto_orientation="none" every entry below would read R0.

orientation_offsets_um = {}
for name, (_x, _y, orientation) in report["placements"].items():
    instance = top.insts[name]
    bbox = instance.dbbox()
    center_x = (bbox.left + bbox.right) / 2
    orientation_offsets_um[name] = (
        orientation,
        instance.ports["d"].dcenter[0] - center_x,
    )

orientation_offsets_um
{'left_0': ('R0', 1.5),
 'left_1': ('R0', 1.5),
 'right_0': ('MY', -1.5),
 'right_1': ('MY', -1.5)}

Place abstract geometry without a KCell

place_components_dbu preserves the public livewire-facing signature. Inputs and outputs are plain DBU dictionaries, so an external layout editor can apply the returned center positions and orientations itself.

geometry_report = dr.place_components_dbu(
    instances=[
        {
            "name": f"block_{index}",
            "center_dbu": (0, 0),
            "size_dbu": (6_000, 8_000),
            "group": "analog_pair",
        }
        for index in range(4)
    ],
    nets=[
        {
            "name": "pair_0",
            "pins": [{"inst": "block_0"}, {"inst": "block_1"}],
        },
        {
            "name": "pair_1",
            "pins": [{"inst": "block_2"}, {"inst": "block_3"}],
        },
    ],
    mode="auto",
    objective={"grouping_weight": 1.0},
    constraints={"min_spacing": 1_000, "density_target": 0.5},
    iterations=40,
)

assert geometry_report["legalized"], geometry_report["placement_violations"]
geometry_report["placements"]
{'block_0': (-3500, 4672, 'R0'),
 'block_1': (10500, 4672, 'R0'),
 'block_2': (-10500, 4672, 'R0'),
 'block_3': (3500, 4672, 'R0')}