Pin access: --access-cost, vias in pads inside wire keepouts, exact stub checks
--access-cost C makes a route pay C to pass where it would close another pin's way out (its end cells, the via it may need there, and the clearance around both) while that pin still has connections to route. A pin inside a wire keepout can only leave by a via; its stub to the cell centre was track inside the keepout, which the exact check refused. The via now goes on the pin itself. Pin stubs are also checked exactly against wire keepouts, as the exact engine will check them. bench/diagnose.py reports what each refused connection collided with.
This commit is contained in:
+2
-2
@@ -24,9 +24,11 @@ off. Exits non-zero if any run crashed or timed out.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
@@ -34,8 +36,6 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
NOT_ROUTED = re.compile(r"freeroute: (\d+) connection\(s\) not routed")
|
||||
TO_ZONE = re.compile(r"freeroute: (\d+) more connection\(s\) left to a zone fill")
|
||||
|
||||
+57
-2
@@ -24,6 +24,7 @@ from pathlib import Path
|
||||
import sys
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.board.search_tree import ShapeSearchTree
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.route import grid_router
|
||||
from freeroute.route.exact_router import route_board_exact
|
||||
@@ -69,6 +70,36 @@ def _capture_router():
|
||||
return seen
|
||||
|
||||
|
||||
def _capture_conflicts() -> list:
|
||||
"""Record each exact-check refusal: the copper box, its kind, and the
|
||||
shape it came too close to. The check stops at a connection's first
|
||||
conflict, so refusals and rejected connections line up in order."""
|
||||
log: list = []
|
||||
original = ShapeSearchTree.clearance_conflict
|
||||
|
||||
def conflict(self, box, net_no, layer, clearance, kind="wire"):
|
||||
hit = original(self, box, net_no, layer, clearance, kind)
|
||||
if hit:
|
||||
expanded = box.offset(clearance)
|
||||
other = next(
|
||||
(s for s in self.overlapping(expanded)
|
||||
if s.net_no != net_no and s.on_layer(layer)
|
||||
and (s.blocks == "all" or s.blocks == kind + "s")),
|
||||
None,
|
||||
)
|
||||
log.append((net_no, kind, layer, box, clearance, other))
|
||||
return hit
|
||||
|
||||
ShapeSearchTree.clearance_conflict = conflict
|
||||
return log
|
||||
|
||||
|
||||
def _gap(a, b) -> int:
|
||||
dx = max(b.ll.x - a.ur.x, a.ll.x - b.ur.x, 0)
|
||||
dy = max(b.ll.y - a.ur.y, a.ll.y - b.ur.y, 0)
|
||||
return max(dx, dy)
|
||||
|
||||
|
||||
def _fresh(seen) -> GridRouter:
|
||||
router = grid_router.GridRouter(*seen["args"], **seen["kwargs"])
|
||||
router._conns = router._build_connections()
|
||||
@@ -111,12 +142,14 @@ def _flood(router: GridRouter, pin, net_no: int, conns) -> tuple[int, Counter]:
|
||||
|
||||
|
||||
def diagnose(
|
||||
dsn_text: str, edge_clearance_mm: float | None, resolution: int = 2, shove: bool = False
|
||||
dsn_text: str, edge_clearance_mm: float | None, resolution: int = 2, shove: bool = False,
|
||||
access_cost: float = 0.0,
|
||||
) -> dict:
|
||||
dsn = parse_dsn(dsn_text)
|
||||
names = {i + 1: n.name for i, n in enumerate(dsn.nets)}
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
seen = _capture_router()
|
||||
conflicts = _capture_conflicts()
|
||||
exact = route_board_exact(
|
||||
build_board(dsn),
|
||||
trace_width=round(_rule_width_dsn(dsn) * scale),
|
||||
@@ -125,6 +158,7 @@ def diagnose(
|
||||
edge_clearance=_mm_to_board(dsn, scale, edge_clearance_mm),
|
||||
resolution=resolution,
|
||||
shove=shove,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
routed = seen["router"]
|
||||
best = routed._diag_best
|
||||
@@ -137,6 +171,19 @@ def diagnose(
|
||||
conns[idx].a_end, conns[idx].b_end = a_end, b_end
|
||||
state._commit(conns[idx], path)
|
||||
empty = _fresh(seen)
|
||||
# refusals in the order the check made them, one per rejected connection
|
||||
per_net: dict[int, list] = {}
|
||||
for net_no, kind, layer, box, clearance, other in conflicts:
|
||||
if other is None:
|
||||
what = "?"
|
||||
elif other.net_no == -1:
|
||||
what = "keepout"
|
||||
else:
|
||||
what = ("trace/via of " if other.routed else "pad of ") + names.get(other.net_no, "?")
|
||||
short = (clearance - _gap(box, other.tile)) / scale / 1000 if other is not None else 0
|
||||
per_net.setdefault(net_no, []).append(
|
||||
{"copper": kind, "layer": layer, "against": what, "short_mm": round(short, 4)}
|
||||
)
|
||||
by_key = {(c.net_no, *_ends_of(c)): c for c in conns}
|
||||
|
||||
rows = []
|
||||
@@ -150,6 +197,8 @@ def diagnose(
|
||||
conn = by_key.get(key)
|
||||
if key not in grid_failed:
|
||||
row["kind"] = "rejected"
|
||||
if per_net.get(net_no):
|
||||
row["conflict"] = per_net[net_no].pop(0)
|
||||
elif conn is None:
|
||||
row["kind"] = "unknown"
|
||||
elif empty._search(empty._conns[conn.index], allow_ripup=False, conns=empty._conns) is None:
|
||||
@@ -177,14 +226,20 @@ def main(argv: list[str] | None = None) -> int:
|
||||
ap.add_argument("--edge-clearance", type=float, metavar="MM")
|
||||
ap.add_argument("--grid-resolution", type=int, default=2, metavar="K")
|
||||
ap.add_argument("--shove", action="store_true", help="as mckicad routes")
|
||||
ap.add_argument("--access-cost", type=float, default=0.0)
|
||||
ap.add_argument("--json", type=Path, help="also write the full table here")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
report = diagnose(args.dsn.read_text(), args.edge_clearance, args.grid_resolution, args.shove)
|
||||
report = diagnose(args.dsn.read_text(), args.edge_clearance, args.grid_resolution, args.shove,
|
||||
args.access_cost)
|
||||
for row in report["failures"]:
|
||||
extra = ""
|
||||
for w in row.get("walled", []):
|
||||
extra += f" [{w['pin']} reaches {w['reach']} cells, walled by {', '.join(w['walls'])}]"
|
||||
if "conflict" in row:
|
||||
c = row["conflict"]
|
||||
extra += (f" [{c['copper']} on layer {c['layer']} {c['short_mm']} mm too close"
|
||||
f" to {c['against']}]")
|
||||
print(f"{row['kind']:11} {row['net']}: {row['a']} to {row['b']}{extra}")
|
||||
total = sum(report["counts"].values())
|
||||
print(f"\n{total} failed: " + ", ".join(f"{k} {v}" for k, v in sorted(report["counts"].items())),
|
||||
|
||||
+13
-2
@@ -30,8 +30,8 @@ from freeroute.ses import write_ses
|
||||
#: which optional flags each engine actually implements. Anything not listed for
|
||||
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
||||
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
||||
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote"}),
|
||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote"}),
|
||||
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote", "access"}),
|
||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access"}),
|
||||
"room": frozenset({"shove", "pack"}),
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ _FLAG_NAMES = {
|
||||
"seed": "--seed",
|
||||
"orders": "--orders",
|
||||
"promote": "--promote-failures",
|
||||
"access": "--access-cost",
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +159,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
help="grid and exact engines: up to R rounds, each routing the connections that "
|
||||
"failed so far first; keeps the best round",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--access-cost",
|
||||
type=float,
|
||||
metavar="C",
|
||||
help="grid and exact engines: extra cost for a route to pass where it would close "
|
||||
"another pin's way out while that pin still has connections to route (default 0: off)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jobs",
|
||||
type=int,
|
||||
@@ -186,6 +194,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
("seed", args.seed is not None),
|
||||
("orders", args.orders is not None),
|
||||
("promote", args.promote_failures is not None),
|
||||
("access", args.access_cost is not None),
|
||||
)
|
||||
if asked
|
||||
]
|
||||
@@ -219,6 +228,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"edge_clearance_mm": args.edge_clearance,
|
||||
"resolution": 2 if args.grid_resolution is None else args.grid_resolution,
|
||||
"heuristic_weight": args.heuristic_weight,
|
||||
"access_cost": args.access_cost or 0.0,
|
||||
}
|
||||
elif args.engine == "grid":
|
||||
options = {
|
||||
@@ -227,6 +237,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"max_passes": max_passes,
|
||||
"edge_clearance_mm": args.edge_clearance,
|
||||
"resolution": 1 if args.grid_resolution is None else args.grid_resolution,
|
||||
"access_cost": args.access_cost or 0.0,
|
||||
}
|
||||
if args.engine == "room":
|
||||
result = build_rooms_routing_result(
|
||||
|
||||
@@ -96,6 +96,7 @@ def route_board_exact(
|
||||
heuristic_weight: float = 1.0,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
@@ -126,6 +127,7 @@ def route_board_exact(
|
||||
heuristic_weight=heuristic_weight,
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
edge = 0 if edge_clearance is None else edge_clearance
|
||||
outline = board.outline
|
||||
|
||||
@@ -157,8 +157,16 @@ class GridRouter:
|
||||
heuristic_weight: float = 1.0,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> None:
|
||||
self.board = board
|
||||
# Pin access: while a pin still has connections to route, the cells
|
||||
# its route would leave by (its end cells, the via it may need there,
|
||||
# and the clearance around both) cost other nets this much extra, so
|
||||
# they route round a fine-pitch pin's way out instead of sealing it.
|
||||
self.access_cost = max(float(access_cost), 0.0)
|
||||
self._doorways: dict[Node, list[tuple[int, int]]] = {}
|
||||
self._pending: dict[int, int] = {}
|
||||
# None keeps the deterministic order; a seed shuffles which connection
|
||||
# routes first, to measure how much a result owes to the order
|
||||
self.seed = seed
|
||||
@@ -289,6 +297,8 @@ class GridRouter:
|
||||
margin = self.clearance + self.half_width
|
||||
outline = self.board.outline
|
||||
self._via_edge_block: set[Cell] = set()
|
||||
# wire keepouts as boxes, to check a pin stub the way the exact engine will
|
||||
self._keepout_boxes: dict[int, list[IntBox]] = {}
|
||||
if outline is not None:
|
||||
# cells too close to the real outline (rounded corners included):
|
||||
# for a trace on every layer, and separately for a via's wider copper
|
||||
@@ -325,6 +335,8 @@ class GridRouter:
|
||||
if box.is_empty():
|
||||
continue
|
||||
# the same distance the exact engine verifies against
|
||||
if item.blocks_wires:
|
||||
self._keepout_boxes.setdefault(item.layer, []).append(box)
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
if item.blocks_wires:
|
||||
self._keepout.add((*cell, item.layer))
|
||||
@@ -474,12 +486,16 @@ class GridRouter:
|
||||
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||
if enter is None:
|
||||
continue
|
||||
if self._doorways:
|
||||
enter += self._access(nxt, net_no)
|
||||
step = 1.0 if dx == 0 or dy == 0 else math.sqrt(2)
|
||||
self._relax(
|
||||
current, nxt, base + step + enter, goal_cell, g_score, came_from, open_heap
|
||||
)
|
||||
if len(self.layers) > 1:
|
||||
g_via = base + self.via_cost
|
||||
if self._doorways:
|
||||
g_via += self._access(current, net_no)
|
||||
for layer in self.layers:
|
||||
if layer == cl:
|
||||
continue
|
||||
@@ -513,6 +529,18 @@ class GridRouter:
|
||||
]
|
||||
|
||||
def _stub_clear(self, pin: Pin, cell: Cell, layer: int, net_no: int, conns) -> bool:
|
||||
keepouts = self._keepout_boxes.get(layer)
|
||||
if keepouts:
|
||||
# the stub is off the grid, so check it exactly, as the exact
|
||||
# engine will: its copper grown by clearance must miss every keepout
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
end = self._cell_center(cell)
|
||||
if end != pin.location:
|
||||
for tile in PolylineShape(Polyline([pin.location, end]), self.half_width).tiles():
|
||||
grown = tile.offset(self.clearance)
|
||||
if any(k.overlaps(grown) for k in keepouts):
|
||||
return False
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
owner = self._cell_owner.get((*c, layer))
|
||||
if owner is not None and conns[owner].net_no != net_no:
|
||||
@@ -765,8 +793,19 @@ class GridRouter:
|
||||
if node not in self._cell_owner:
|
||||
self._cell_owner[node] = conn.index
|
||||
conn.trace_cells.append((c, layer))
|
||||
# A pin inside a wire keepout can only leave by a via. Its stub to the
|
||||
# cell centre would be track inside the keepout, so the via goes on
|
||||
# the pin itself (a via in its own pad) and the stub is left out.
|
||||
in_pad: dict[int, IntPoint] = {}
|
||||
if len(wire_segs) > 1:
|
||||
for idx, pin in ((0, conn.a_end), (len(wire_segs) - 1, conn.b_end)):
|
||||
layer, cells = wire_segs[idx]
|
||||
if isinstance(pin, Pin) and len(cells) == 1 and (*cells[0], layer) in self._keepout:
|
||||
in_pad[idx] = pin.location
|
||||
conn.wires = []
|
||||
for idx, (layer, cells) in enumerate(wire_segs):
|
||||
if idx in in_pad:
|
||||
continue
|
||||
points = [self._cell_center(c) for c in cells]
|
||||
if idx == 0 and isinstance(conn.a_end, Pin):
|
||||
points.insert(0, conn.a_end.location)
|
||||
@@ -776,7 +815,11 @@ class GridRouter:
|
||||
if len(points) >= 2:
|
||||
conn.wires.append((layer, points))
|
||||
conn.vias = [self._cell_center(c) for c in conn.via_cells]
|
||||
for idx, location in in_pad.items():
|
||||
# via k joins wire segments k and k + 1
|
||||
conn.vias[0 if idx == 0 else idx - 1] = location
|
||||
conn.routed = True
|
||||
self._settle(conn, -1)
|
||||
|
||||
def _rip(self, conn: _Connection) -> None:
|
||||
for cell, layer in conn.trace_cells:
|
||||
@@ -799,6 +842,7 @@ class GridRouter:
|
||||
conn.vias = []
|
||||
conn.routed = False
|
||||
conn.rip_count += 1
|
||||
self._settle(conn, +1)
|
||||
|
||||
def _crossed_owners(self, path: list[Node], net_no: int, conns: list[_Connection]) -> set[int]:
|
||||
owners: set[int] = set()
|
||||
@@ -814,6 +858,50 @@ class GridRouter:
|
||||
owners.add(i)
|
||||
return owners
|
||||
|
||||
# --- pin access ---------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _group_pins(conn: _Connection):
|
||||
return [*conn.a_group[0], *conn.b_group[0]]
|
||||
|
||||
def _build_doorways(self, conns: list[_Connection]) -> None:
|
||||
"""Map every cell that would close a pin's way out to that pin."""
|
||||
spread = [(0, 0), *self._halo_offsets]
|
||||
done: set[int] = set()
|
||||
for conn in conns:
|
||||
for pin in self._group_pins(conn):
|
||||
self._pending[id(pin)] = self._pending.get(id(pin), 0) + 1
|
||||
if id(pin) in done:
|
||||
continue
|
||||
done.add(id(pin))
|
||||
claim = (id(pin), conn.net_no)
|
||||
exits: set[Node] = set()
|
||||
for x, y, layer in self._end_nodes(pin, conn.net_no, conns):
|
||||
exits.add((x, y, layer))
|
||||
for c in self._via_cells((x, y)):
|
||||
exits.update((*c, other) for other in self.layers)
|
||||
cells: set[Node] = set()
|
||||
for x, y, layer in exits:
|
||||
for dx, dy in spread:
|
||||
if 0 <= x + dx < self.cols and 0 <= y + dy < self.rows:
|
||||
cells.add((x + dx, y + dy, layer))
|
||||
for node in cells:
|
||||
self._doorways.setdefault(node, []).append(claim)
|
||||
|
||||
def _settle(self, conn: _Connection, delta: int) -> None:
|
||||
if not self._pending:
|
||||
return
|
||||
for pin in self._group_pins(conn):
|
||||
if id(pin) in self._pending:
|
||||
self._pending[id(pin)] += delta
|
||||
|
||||
def _access(self, node: Node, net_no: int) -> float:
|
||||
"""Extra cost for ``net_no`` to use a cell another pin still needs."""
|
||||
for pin_id, owner_net in self._doorways.get(node, ()):
|
||||
if owner_net != net_no and self._pending.get(pin_id, 0) > 0:
|
||||
return self.access_cost
|
||||
return 0.0
|
||||
|
||||
# --- passes -------------------------------------------------------------
|
||||
|
||||
def route(self) -> RouteResult:
|
||||
@@ -822,6 +910,8 @@ class GridRouter:
|
||||
self._conns = conns
|
||||
if not conns:
|
||||
return RouteResult(self.half_width)
|
||||
if self.access_cost:
|
||||
self._build_doorways(conns)
|
||||
|
||||
passes = self.max_passes if self.rip_up else 1
|
||||
best_count = -1
|
||||
@@ -1067,6 +1157,7 @@ def route_board(
|
||||
heuristic_weight: float = 1.0,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -1084,5 +1175,6 @@ def route_board(
|
||||
heuristic_weight=heuristic_weight,
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
@@ -82,6 +82,7 @@ def route_dsn_board(
|
||||
max_passes: int = 10,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> tuple[RouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||
@@ -104,6 +105,7 @@ def route_dsn_board(
|
||||
max_passes=max_passes,
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
return result, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -146,12 +148,14 @@ def build_routing_result(
|
||||
edge_clearance_mm: float | None = None,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||
route, scale, layer_names = route_dsn_board(
|
||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
||||
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
return _to_routing_result(route, dsn, scale, layer_names)
|
||||
|
||||
@@ -169,6 +173,7 @@ def route_dsn_board_exact(
|
||||
diagonal: bool = False,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||
@@ -195,6 +200,7 @@ def route_dsn_board_exact(
|
||||
diagonal=diagonal,
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -212,6 +218,7 @@ def build_exact_routing_result(
|
||||
edge_clearance_mm: float | None = None,
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(
|
||||
@@ -226,6 +233,7 @@ def build_exact_routing_result(
|
||||
heuristic_weight=heuristic_weight,
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
|
||||
"seed": ["--seed", "3"],
|
||||
"orders": ["--orders", "2"],
|
||||
"promote": ["--promote-failures", "2"],
|
||||
"access": ["--access-cost", "5"],
|
||||
}
|
||||
for engine, options in ENGINE_OPTIONS.items():
|
||||
for option in options:
|
||||
|
||||
Reference in New Issue
Block a user