add_bundle_astar

add_bundle_astar(
    component: ProtoTKCell,
    ports1: list[PortLike],
    ports2: list[PortLike],
    straight: KCellSpec,
    bend: KCellSpec,
    layers: Iterable[LayerLike],
    grid_unit: Int = 500,
    *,
    spacing: Length,
    clearance: Length = 0.0,
    fan_in: FanStrategy = "manhattan",
    fan_out: FanStrategy | None = None,
    fanin_strategy: FanStrategyType | None = None,
    fanin_strategy_kwargs: dict[str, Any] | None = None,
    return_corners: Any = _UNSET,
    fanout_strategy: FanStrategyType | None = None,
    fanout_strategy_kwargs: dict[str, Any] | None = None,
) -> BundleResult | list[None]

Add a bundle route using the a-star algorithm.

Parameters:

Name Type Description Default
component ProtoTKCell

The component to add the route into.

required
ports1 list[PortLike]

the start ports

required
ports2 list[PortLike]

the end ports

required
straight KCellSpec

the straight-spec to create straights with

required
bend KCellSpec

the bend-spec to create bends with

required
layers Iterable[LayerLike]

the layers to avoid.

required
grid_unit Int

the discretization unit for the a-star algorithm.

500
spacing Length

the spacing between the waveguides in the bundle

required
clearance Length

extra space (um) between the bundle body and any obstacle on layers. Each obstacle polygon is grown by clearance (via kdb.Region.sized) before A* discretization, so the bundle stays clearance away from every obstacle edge. 0.0 (default) preserves the prior behaviour of letting the bundle share an edge with obstacles.

0.0
fan_in FanStrategy

how the start (ports1) bundle is formed. A string ("manhattan", "sbend", "lbend") or a :class:FanStrategySpec dict with a "type" key and optional per-strategy parameters.

'manhattan'
fan_out FanStrategy | None

how the end (ports2) bundle is formed. Same options as fan_in. None (default) inherits fan_in so existing call sites keep working; pass an explicit value to mix strategies.

None

Returns:

Name Type Description
A BundleResult | list[None]

class:BundleResult with per-trace corner polylines,

BundleResult | list[None]

path lengths, and the bundle centerline.

Source code in doroutes/bundles.py
def add_bundle_astar(
    component: ProtoTKCell,
    ports1: list[PortLike],
    ports2: list[PortLike],
    straight: KCellSpec,
    bend: KCellSpec,
    layers: Iterable[LayerLike],
    grid_unit: Int = 500,
    *,
    spacing: Length,
    clearance: Length = 0.0,
    fan_in: FanStrategy = "manhattan",
    fan_out: FanStrategy | None = None,
    # --- old kwargs accepted for backward compat ---
    fanin_strategy: FanStrategyType | None = None,
    fanin_strategy_kwargs: dict[str, Any] | None = None,
    return_corners: Any = _UNSET,
    fanout_strategy: FanStrategyType | None = None,
    fanout_strategy_kwargs: dict[str, Any] | None = None,
) -> BundleResult | list[None]:
    """Add a bundle route using the a-star algorithm.

    Args:
        component: The component to add the route into.
        ports1: the start ports
        ports2: the end ports
        straight: the straight-spec to create straights with
        bend: the bend-spec to create bends with
        layers: the layers to avoid.
        grid_unit: the discretization unit for the a-star algorithm.
        spacing: the spacing between the waveguides in the bundle
        clearance: extra space (um) between the bundle body and any
            obstacle on ``layers``. Each obstacle polygon is grown by
            ``clearance`` (via ``kdb.Region.sized``) before A*
            discretization, so the bundle stays ``clearance`` away
            from every obstacle edge. ``0.0`` (default) preserves the
            prior behaviour of letting the bundle share an edge with
            obstacles.
        fan_in: how the start (``ports1``) bundle is formed. A string
            (``"manhattan"``, ``"sbend"``, ``"lbend"``) or a
            :class:`FanStrategySpec` dict with a ``"type"`` key and
            optional per-strategy parameters.
        fan_out: how the end (``ports2``) bundle is formed. Same
            options as ``fan_in``. ``None`` (default) inherits
            ``fan_in`` so existing call sites keep working;
            pass an explicit value to mix strategies.

    Returns:
        A :class:`BundleResult` with per-trace corner polylines,
        path lengths, and the bundle centerline.

    """
    # Handle deprecated fanin_strategy / fanout_strategy kwargs
    if fanin_strategy is not None:
        warnings.warn(
            "fanin_strategy/fanin_strategy_kwargs are deprecated, "
            "use fan_in=FanStrategy instead",
            DeprecationWarning,
            stacklevel=2,
        )
        fan_in = _old_params_to_fan_strategy(
            fanin_strategy, fanin_strategy_kwargs, component.kcl
        )
    if fanout_strategy is not None:
        if fanin_strategy is None:
            warnings.warn(
                "fanout_strategy/fanout_strategy_kwargs are deprecated, "
                "use fan_out=FanStrategy instead",
                DeprecationWarning,
                stacklevel=2,
            )
        fan_out = _old_params_to_fan_strategy(
            fanout_strategy, fanout_strategy_kwargs, component.kcl
        )

    if return_corners is not _UNSET:
        warnings.warn(
            "return_corners is deprecated; add_bundle_astar now always "
            "returns the corners in um",
            DeprecationWarning,
            stacklevel=2,
        )

    result = _add_bundle_astar_impl(
        component=component,
        ports1=ports1,
        ports2=ports2,
        straight=straight,
        bend=bend,
        layers=layers,
        grid_unit=grid_unit,
        spacing=spacing,
        clearance=clearance,
        fan_in=fan_in,
        fan_out=fan_out,
    )

    if return_corners is not _UNSET and not return_corners:
        return [None for _ in range(len(ports1))]
    return result

add_fan_in

add_fan_in(
    component: ProtoTKCell,
    inputs: list[PortLike],
    straight: KCellSpec,
    bend: KCellSpec,
    *,
    x_bundle: Length | None = None,
    y_bundle: Length | None = None,
    spacing: Length | None = None,
    start_dir: OrientationChar | None = None,
    strategy: FanStrategy = "manhattan",
) -> NDArray[int64]

Add a fan-in to a parent component.

Parameters:

Name Type Description Default
component ProtoTKCell

the component to add the fan-in to

required
inputs list[PortLike]

the list of ports to start from

required
straight KCellSpec

the straight-spec to create straights from

required
bend KCellSpec

the bend-spec to create bends from

required
x_bundle Length | None

the x-location (µm) where to form the confluence of the bundle. Will be two bend radii from the inputs if not given.

None
y_bundle Length | None

the y-location (µm) where to form the confluence of the bundle. Will be somewhere in the middle if not given.

None
spacing Length | None

the spacing (µm) between waveguides in the bundle

None
start_dir OrientationChar | None

the start direction of the bundle (derived from ports if not given)

None
strategy FanStrategy

fan-in strategy. A string ("manhattan", "sbend", or "lbend") or a :class:FanStrategySpec dict with a "type" key and optional per-strategy parameters. "manhattan" uses the existing 90°-comb fan-in. "sbend" compresses each wire to the bundle line with a single S-bend per port (useful when the bend radius is tight relative to the port pitch). "lbend" forms an L-shaped per-port fan-in (variable straight + 90° bend + variable straight) so virtual ports land on a common line at spacing pitch with the bundle direction rotated 90° from the input ports. "lbend" requires an explicit lbend_side ("left" or "right") in the strategy dict; "auto" resolution is only available via add_bundle_astar (which sees both port sets). Strategy-specific options (e.g. start_straight, lbend_side, sbend_length) are passed inside the dict, e.g. strategy={"type": "manhattan", "start_straight": 5.0}.

'manhattan'
Source code in doroutes/fanin.py
def add_fan_in(
    component: ProtoTKCell,
    inputs: list[PortLike],
    straight: KCellSpec,
    bend: KCellSpec,
    *,
    x_bundle: Length | None = None,
    y_bundle: Length | None = None,
    spacing: Length | None = None,
    start_dir: OrientationChar | None = None,
    strategy: FanStrategy = "manhattan",
) -> NDArray[np.int64]:
    """Add a fan-in to a parent component.

    Args:
        component: the component to add the fan-in to
        inputs: the list of ports to start from
        straight: the straight-spec to create straights from
        bend: the bend-spec to create bends from
        x_bundle: the x-location (µm) where to form the confluence of the bundle.
            Will be two bend radii from the inputs if not given.
        y_bundle: the y-location (µm) where to form the confluence of the bundle.
            Will be somewhere in the middle if not given.
        spacing: the spacing (µm) between waveguides in the bundle
        start_dir: the start direction of the bundle (derived from ports if not given)
        strategy: fan-in strategy. A string (``"manhattan"``, ``"sbend"``, or
            ``"lbend"``) or a :class:`FanStrategySpec` dict with a ``"type"``
            key and optional per-strategy parameters. ``"manhattan"`` uses the
            existing 90°-comb fan-in. ``"sbend"`` compresses each wire to the
            bundle line with a single S-bend per port (useful when the bend
            radius is tight relative to the port pitch). ``"lbend"`` forms an
            L-shaped per-port fan-in (variable straight + 90° bend + variable
            straight) so virtual ports land on a common line at ``spacing``
            pitch with the bundle direction rotated 90° from the input ports.
            ``"lbend"`` requires an explicit ``lbend_side`` (``"left"`` or
            ``"right"``) in the strategy dict; ``"auto"`` resolution is only
            available via ``add_bundle_astar`` (which sees both port sets).
            Strategy-specific options (e.g. ``start_straight``, ``lbend_side``,
            ``sbend_length``) are passed inside the dict, e.g.
            ``strategy={"type": "manhattan", "start_straight": 5.0}``.

    """
    kc: KCell = util.as_kcell(component)
    # Resolve FanStrategy → (type, strategy_kwargs_dbu, anchor_kwargs_dbu)
    strategy_type, strategy_kwargs, anchor_kwargs = resolve_fan_strategy(
        strategy, kc.kcl
    )

    # Convert explicit um params to dbu; anchor from FanStrategySpec is fallback
    x_bundle_dbu: Dbu | None = (
        util.to_dbu(kc.kcl, x_bundle)
        if x_bundle is not None
        else anchor_kwargs.get("x_bundle_dbu")
    )
    y_bundle_dbu: Dbu | None = (
        util.to_dbu(kc.kcl, y_bundle)
        if y_bundle is not None
        else anchor_kwargs.get("y_bundle_dbu")
    )
    spacing_dbu: Dbu | None = (
        util.to_dbu(kc.kcl, spacing) if spacing is not None else None
    )

    d0 = _resolve_start_direction(inputs, start_dir)
    coord = 1 if d0 in "ew" else 0
    starts_with_o = [validate_position_with_orientation(p) for p in inputs]
    starts = np.array(
        sorted([(x, y) for x, y, _ in starts_with_o], key=lambda xy: xy[coord])
    )

    if strategy_type == "manhattan":
        return _add_fan_in_manhattan(
            kc,
            starts,
            d0,
            straight,
            bend,
            x_bundle_dbu,
            y_bundle_dbu,
            spacing_dbu,
            start_straight_dbu=int(strategy_kwargs.get("start_straight_dbu", 0)),
        )
    if strategy_type == "sbend":
        # Sort the original Port objects by the same key as ``starts`` so the
        # sbend implementation can connect to actual port instances.
        sorted_inputs = sorted(
            inputs,
            key=lambda p: validate_position_with_orientation(p)[coord],
        )
        return _add_fan_in_sbend(
            kc,
            starts,
            sorted_inputs,
            d0,
            straight,
            bend,
            x_bundle_dbu,
            y_bundle_dbu,
            spacing_dbu,
            **strategy_kwargs,
        )
    if strategy_type == "lbend":
        return _add_fan_in_lbend(
            kc,
            inputs,
            d0,
            straight,
            bend,
            spacing_dbu,
            **strategy_kwargs,
        )
    msg = f"Unknown fan-in strategy: {strategy_type!r}"
    raise ValueError(msg)

add_route_astar

add_route_astar(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    straight: KCellSpec,
    bend: KCellSpec,
    layers: Iterable[LayerLike],
    grid_unit: Dbu,
    *,
    clearance: Length = 0.0,
) -> PointsUm

Add an a-star route to a component.

Parameters:

Name Type Description Default
component ProtoTKCell

the component to add the route to

required
start PortLike

the start port

required
stop PortLike

the stop port

required
straight KCellSpec

the straight-spec to create straights from

required
bend KCellSpec

the bend-spec to create bends from

required
layers Iterable[LayerLike]

the layers to avoid

required
grid_unit Dbu

the discretization unit for the a-star algorithm

required
clearance Length

extra space (µm) between the route body and any obstacle on layers. Each obstacle polygon is grown by clearance before A* discretization, so the route stays clearance away from every obstacle edge. 0.0 (default) preserves the prior behaviour of letting the route share an edge with obstacles.

0.0

Returns:

Type Description
PointsUm

The routed corner points in µm.

Source code in doroutes/routing.py
def add_route_astar(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    straight: KCellSpec,
    bend: KCellSpec,
    layers: Iterable[LayerLike],
    grid_unit: Dbu,
    *,
    clearance: Length = 0.0,
) -> PointsUm:
    """Add an a-star route to a component.

    Args:
        component: the component to add the route to
        start: the start port
        stop: the stop port
        straight: the straight-spec to create straights from
        bend: the bend-spec to create bends from
        layers: the layers to avoid
        grid_unit: the discretization unit for the a-star algorithm
        clearance: extra space (µm) between the route body and any
            obstacle on ``layers``. Each obstacle polygon is grown by
            ``clearance`` before A* discretization, so the route stays
            ``clearance`` away from every obstacle edge. ``0.0``
            (default) preserves the prior behaviour of letting the
            route share an edge with obstacles.

    Returns:
        The routed corner points in µm.

    """
    corners = find_route_astar(
        component, start, stop, straight, bend, layers, grid_unit, clearance=clearance
    )
    _start = validate_position_with_orientation(start)
    _stop = validate_position_with_orientation(stop)
    _add_route_from_corners_impl(
        c=component,
        start=(_start[0], _start[1]),
        stop=(_stop[0], _stop[1]),
        corners=corners,
        straight=straight,
        bend=bend,
    )
    return [
        (util.to_um(component.kcl, x), util.to_um(component.kcl, y)) for x, y in corners
    ]

add_route_from_corners

add_route_from_corners(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    corners: list[tuple[Length, Length]] | None = None,
    *,
    straight: KCellSpec,
    bend: KCellSpec,
    return_corners: bool = False,
) -> PointsUm | None

Deprecated: use add_route_manual(corners=...).

Source code in doroutes/routing.py
def add_route_from_corners(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    corners: list[tuple[Length, Length]] | None = None,
    *,
    straight: KCellSpec,
    bend: KCellSpec,
    return_corners: bool = False,
) -> PointsUm | None:
    """Deprecated: use ``add_route_manual(corners=...)``."""
    warnings.warn(
        "add_route_from_corners is deprecated, use add_route_manual(corners=...)",
        DeprecationWarning,
        stacklevel=2,
    )
    result = _add_route_from_corners(
        component, start, stop, corners, straight=straight, bend=bend
    )
    return result if return_corners else None

add_route_from_steps

add_route_from_steps(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    steps: list[Step],
    *,
    straight: KCellSpec,
    bend: KCellSpec,
    return_corners: bool = False,
) -> PointsUm | None

Deprecated: use add_route_manual(steps=...).

Source code in doroutes/routing.py
def add_route_from_steps(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    steps: list[Step],
    *,
    straight: KCellSpec,
    bend: KCellSpec,
    return_corners: bool = False,
) -> PointsUm | None:
    """Deprecated: use ``add_route_manual(steps=...)``."""
    warnings.warn(
        "add_route_from_steps is deprecated, use add_route_manual(steps=...)",
        DeprecationWarning,
        stacklevel=2,
    )
    result = _add_route_from_steps(
        component, start, stop, steps, straight=straight, bend=bend
    )
    return result if return_corners else None

find_route_astar

find_route_astar(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    straight: KCellSpec,
    bend: KCellSpec,
    layers: Iterable[LayerLike],
    grid_unit: Dbu,
    *,
    clearance: Length = 0.0,
) -> PointsDbu

Find an a-star route without adding it to the component.

Parameters:

Name Type Description Default
component ProtoTKCell

the component to find the route in

required
start PortLike

the start port

required
stop PortLike

the stop port

required
straight KCellSpec

the straight-spec to create straights from

required
bend KCellSpec

the bend-spec to create bends from

required
layers Iterable[LayerLike]

the layers to avoid

required
grid_unit Dbu

the discretization unit for the a-star algorithm

required
clearance Length

extra space (µm) between the route body and any obstacle on layers. Implemented by polygon-growing the obstacles via kdb.Region.sized before A* discretization (the same mechanism the multilayer engines use).

0.0

Returns:

Type Description
PointsDbu

The corners of the route as a list of points in dbu.

Source code in doroutes/routing.py
def find_route_astar(
    component: ProtoTKCell,
    start: PortLike,
    stop: PortLike,
    straight: KCellSpec,
    bend: KCellSpec,
    layers: Iterable[LayerLike],
    grid_unit: Dbu,
    *,
    clearance: Length = 0.0,
) -> PointsDbu:
    """Find an a-star route without adding it to the component.

    Args:
        component: the component to find the route in
        start: the start port
        stop: the stop port
        straight: the straight-spec to create straights from
        bend: the bend-spec to create bends from
        layers: the layers to avoid
        grid_unit: the discretization unit for the a-star algorithm
        clearance: extra space (µm) between the route body and any
            obstacle on ``layers``. Implemented by polygon-growing the
            obstacles via ``kdb.Region.sized`` before A* discretization
            (the same mechanism the multilayer engines use).

    Returns:
        The corners of the route as a list of points in dbu.

    """
    kc: KCell = util.as_kcell(component)
    _layers = [validate_layer(kc.kcl, layer) for layer in layers]
    width_dbu = util.extract_waveguide_width(kc.kcl, straight)
    radius_dbu = util.extract_bend_radius(kc.kcl, bend)
    if grid_unit > 0.5 * radius_dbu:
        msg = "bend radius should at least be twice the grid unit."
        raise ValueError(msg)
    _grid_unit = int(radius_dbu / int(radius_dbu / grid_unit))
    buffer_dbu = max(0, util.to_dbu(kc.kcl, clearance))
    bbox = kc.bbox()
    straight_width = width_dbu // grid_unit + 1
    straight_width += (straight_width + 1) % 2
    _bend = util.discretize_bend(kc.kcl, bend, _grid_unit, _layers)
    _start = validate_position_with_orientation(start)
    _stop = validate_position_with_orientation(
        stop, invert_orientation=isinstance(stop, Port | DPort)
    )
    exclude_points = [
        (int(_start[0]), int(_start[1])),
        (int(_stop[0]), int(_stop[1])),
    ]
    return _doroutes.show(
        polys=util.extract_polys(
            kc, _layers, buffer_dbu=buffer_dbu, exclude_points_dbu=exclude_points
        ),
        bbox=(
            bbox.top + 2 * radius_dbu,
            bbox.right + 2 * radius_dbu,
            bbox.bottom - 2 * radius_dbu,
            bbox.left - 2 * radius_dbu,
        ),
        start=_start,
        stop=_stop,
        grid_unit=_grid_unit,
        straight_width=straight_width,
        discretized_bend_east_to_north=_bend,
    )

place_auto

place_auto(
    c: ProtoTKCell,
    instances: list[str] | list[PlacedInstanceSpec],
    nets: list[PlacementNetSpec] | list[object],
    *,
    mode: PlacementMode = "auto",
    objective: PlacementObjective | None = None,
    constraints: PlacementConstraints | None = None,
    routing_feedback: RoutingFeedbackConfig | None = None,
    apply: bool = True,
    iterations: int = 30,
) -> PlacementReport

Automatically place instances using openroad-style or tiling mode.

Parameters:

Name Type Description Default
c ProtoTKCell

component/KCell to place instances in.

required
instances list[str] | list[PlacedInstanceSpec]

list of instance names or specs.

required
nets list[PlacementNetSpec] | list[object]

placement netlist — either PlacementNetSpec dicts (pins refer to instance names) or RouteNetSpec objects. When RouteNetSpec objects are given they are auto-converted via derive_placement_nets().

required
mode PlacementMode

"openroad", "tiling", or "auto".

'auto'
objective PlacementObjective | None

objective weights.

None
constraints PlacementConstraints | None

placement constraints.

None
routing_feedback RoutingFeedbackConfig | None

reserved for future routing-in-loop strategies.

None
apply bool

apply placement in-place when True.

True
iterations int

optimizer iteration budget.

30

Returns:

Type Description
PlacementReport

Structured placement report with final locations and metrics.

Source code in doroutes/placement.py
def place_auto(
    c: ProtoTKCell,
    instances: list[str] | list[PlacedInstanceSpec],
    nets: list[PlacementNetSpec] | list[object],
    *,
    mode: PlacementMode = "auto",
    objective: PlacementObjective | None = None,
    constraints: PlacementConstraints | None = None,
    routing_feedback: RoutingFeedbackConfig | None = None,
    apply: bool = True,
    iterations: int = 30,
) -> PlacementReport:
    """Automatically place instances using openroad-style or tiling mode.

    Args:
        c: component/KCell to place instances in.
        instances: list of instance names or specs.
        nets: placement netlist — either ``PlacementNetSpec`` dicts (pins
            refer to instance names) or ``RouteNetSpec`` objects.  When
            ``RouteNetSpec`` objects are given they are auto-converted via
            ``derive_placement_nets()``.
        mode: "openroad", "tiling", or "auto".
        objective: objective weights.
        constraints: placement constraints.
        routing_feedback: reserved for future routing-in-loop strategies.
        apply: apply placement in-place when True.
        iterations: optimizer iteration budget.

    Returns:
        Structured placement report with final locations and metrics.

    """
    del routing_feedback  # v1 uses congestion-proxy feedback internally.

    kc = _as_kcell(c)

    constraints = constraints or {}
    objective = objective or {}

    # Auto-convert RouteNetSpec objects to PlacementNetSpec dicts.
    if nets and hasattr(nets[0], "start") and hasattr(nets[0], "stop"):
        # Collect instance names for derive_placement_nets.
        inst_names: list[str] = []
        if instances and isinstance(instances[0], str):
            inst_names = list(instances)  # type: ignore[arg-type]
        else:
            inst_names = [spec["name"] for spec in instances]  # type: ignore[index]
        nets = derive_placement_nets(nets, inst_names)  # type: ignore[arg-type]

    pnets = cast(list[PlacementNetSpec], nets)

    # Normalize instance specs.
    inst_specs: list[PlacedInstanceSpec] = []
    if instances and isinstance(instances[0], str):
        inst_specs = [{"name": str(name)} for name in instances]  # type: ignore[arg-type]
    else:
        inst_specs = list(instances)  # type: ignore[assignment]

    rust_instances: list[tuple[str, int, int, int, int, bool, int, str]] = []
    name_to_idx: dict[str, int] = {}
    current_centers: dict[str, tuple[int, int]] = {}
    inst_sizes: dict[str, tuple[int, int]] = {}

    for idx, spec in enumerate(inst_specs):
        name = spec["name"]
        try:
            inst = kc.insts[name]
        except (
            Exception
        ) as e:  # pragma: no cover - kfactory error type depends on backend
            msg = f"Instance '{name}' not found in component."
            raise ValueError(msg) from e

        x, y, width, height = _instance_center_and_size(inst)
        fixed = bool(spec.get("fixed", False))
        keepout = int(spec.get("keepout", 0))
        group = str(spec.get("group", ""))
        rust_instances.append((name, x, y, width, height, fixed, keepout, group))
        name_to_idx[name] = idx
        current_centers[name] = (x, y)
        inst_sizes[name] = (width, height)

    # Normalize net specs to rust format: pins reference instance indices.
    rust_nets: list[tuple[str, list[tuple[int, float]], float]] = []
    for net in pnets:
        pin_refs: list[tuple[int, float]] = []
        net_weight = float(net.get("weight", 1.0))
        for pin in net["pins"]:
            inst_name = _pin_to_instance_name(pin)
            if inst_name not in name_to_idx:
                msg = f"Net '{net['name']}' references unknown instance '{inst_name}'."
                raise ValueError(msg)
            pin_refs.append((name_to_idx[inst_name], 1.0))
        if len(pin_refs) >= 2:
            rust_nets.append((net["name"], pin_refs, net_weight))

    if "bbox" in constraints:
        bbox = constraints["bbox"]
    else:
        cb = kc.bbox()
        bbox = (int(cb.top), int(cb.right), int(cb.bottom), int(cb.left))

    if not hasattr(_doroutes, "place_batch"):
        msg = (
            "doroutes extension does not expose place_batch yet. "
            "Rebuild the Rust extension (e.g. `maturin develop`) and retry."
        )
        raise RuntimeError(msg)

    placements, mode_used, hpwl, congestion, density_ovf, legalized, iter_metrics = (
        _doroutes.place_batch(
            instances=rust_instances,
            nets=rust_nets,
            bbox=bbox,
            mode=mode,
            min_spacing=int(constraints.get("min_spacing", 0)),
            density_target=float(constraints.get("density_target", 0.7)),
            iterations=max(1, int(iterations)),
            hpwl_weight=float(objective.get("hpwl_weight", 1.0)),
            congestion_weight=float(objective.get("congestion_weight", 1.2)),
            density_weight=float(objective.get("density_weight", 1.0)),
            displacement_weight=float(objective.get("displacement_weight", 0.15)),
            grouping_weight=float(objective.get("grouping_weight", 0.0)),
            timing_driven=bool(objective.get("timing_driven", False)),
            routability_driven=bool(objective.get("routability_driven", False)),
            max_displacement=int(constraints.get("max_displacement", 0)),
        )
    )

    placement_map = {name: (x, y, orient) for name, x, y, orient in placements}

    # Post-process: abut same-group instances with min_spacing gap.
    placement_map = _abut_groups(
        placement_map,
        inst_specs,
        inst_sizes,
        min_gap=int(constraints.get("min_spacing", 0)),
    )

    if apply:
        for name, (x, y, _orient) in placement_map.items():
            inst = kc.insts[name]
            curx, cury = current_centers[name]
            dx = x - curx
            dy = y - cury
            inst.dmove((util.to_um(kc.kcl, dx), util.to_um(kc.kcl, dy)))

    return {
        "mode_used": mode_used,
        "placements": placement_map,
        "hpwl": float(hpwl),
        "congestion": float(congestion),
        "density_overflow": float(density_ovf),
        "legalized": bool(legalized),
        "iterations": [
            (int(i), float(h), float(cg), float(d)) for i, h, cg, d in iter_metrics
        ],
    }

place_openroad_style

place_openroad_style(
    c: ProtoTKCell,
    instances: list[str] | list[PlacedInstanceSpec],
    nets: list[PlacementNetSpec],
    **kwargs: Any,
) -> PlacementReport

Run place_auto with mode="openroad".

Source code in doroutes/placement.py
def place_openroad_style(
    c: ProtoTKCell,
    instances: list[str] | list[PlacedInstanceSpec],
    nets: list[PlacementNetSpec],
    **kwargs: Any,
) -> PlacementReport:
    """Run `place_auto` with ``mode="openroad"``."""
    return place_auto(c, instances, nets, mode="openroad", **kwargs)

place_tiling

place_tiling(
    c: ProtoTKCell,
    instances: list[str] | list[PlacedInstanceSpec],
    nets: list[PlacementNetSpec],
    **kwargs: Any,
) -> PlacementReport

Run place_auto with mode="tiling".

Source code in doroutes/placement.py
def place_tiling(
    c: ProtoTKCell,
    instances: list[str] | list[PlacedInstanceSpec],
    nets: list[PlacementNetSpec],
    **kwargs: Any,
) -> PlacementReport:
    """Run `place_auto` with ``mode="tiling"``."""
    return place_auto(c, instances, nets, mode="tiling", **kwargs)