--rollback: undo a rip-up that leaves fewer connections routed

The connections a rip-up route crosses are rerouted at once in free
space. If that leaves fewer connections routed than before, the new
route comes out and every displaced connection goes back on its old
path, with its rip count as it was. Even trades are kept.
This commit is contained in:
2026-09-28 14:54:29 -06:00
parent 35891c02ff
commit 3462089e09
6 changed files with 109 additions and 4 deletions
+43 -2
View File
@@ -113,6 +113,7 @@ class _Connection:
"b_group",
"a_end",
"b_end",
"path",
)
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
@@ -134,6 +135,8 @@ class _Connection:
self.b_group: tuple[list[Pin], list] = ([b], [])
self.a_end: Pin | None = a
self.b_end: Pin | None = b
# the node path last committed, so a rip-up can be undone
self.path: list[Node] | None = None
class GridRouter:
@@ -159,6 +162,7 @@ class GridRouter:
seed: int | None = None,
route_first: frozenset | None = None,
access_cost: float = 0.0,
rollback: bool = False,
) -> None:
self.board = board
# Pin access: while a pin still has connections to route, the cells
@@ -168,6 +172,10 @@ class GridRouter:
self.access_cost = max(float(access_cost), 0.0)
self._doorways: dict[Node, list[tuple[int, int]]] = {}
self._pending: dict[int, int] = {}
# Rip-up as a transaction: the connections a rip-up route crosses are
# rerouted at once, and if fewer connections are routed afterwards
# than before, everything goes back as it was.
self.rollback = bool(rollback)
# 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
@@ -788,6 +796,7 @@ class GridRouter:
# --- commit / rip -------------------------------------------------------
def _commit(self, conn: _Connection, path: list[Node]) -> None:
conn.path = list(path)
wire_segs, via_cells = _split_path(path)
via_cells = list(via_cells)
if isinstance(conn.a_end, ConductionArea):
@@ -981,11 +990,41 @@ class GridRouter:
path = self._search(conn, allow_ripup=True, conns=conns)
if path is None:
return False
for idx in sorted(self._crossed_owners(path, conn.net_no, conns)):
self._rip(conns[idx])
victims = [conns[i] for i in sorted(self._crossed_owners(path, conn.net_no, conns))]
if self.rollback:
return self._ripup_transaction(conn, path, victims, conns)
for victim in victims:
self._rip(victim)
self._commit(conn, path)
return True
def _ripup_transaction(self, conn, path, victims, conns) -> bool:
"""Route ``conn`` through ``victims``, reroute them at once in free
space, and undo it all if fewer connections end up routed."""
saved = [(v, v.path, v.a_end, v.b_end, v.rip_count) for v in victims]
own_rips = conn.rip_count
for victim in victims:
self._rip(victim)
self._commit(conn, path)
rerouted = 0
for victim in victims:
again = self._search(victim, allow_ripup=False, conns=conns)
if again is not None:
self._commit(victim, again)
rerouted += 1
if 1 + rerouted >= len(victims):
return True # a gain, or an even trade that may pay off later
for victim in victims:
if victim.routed:
self._rip(victim)
self._rip(conn)
conn.rip_count = own_rips
for victim, old_path, a_end, b_end, rips in saved:
victim.a_end, victim.b_end = a_end, b_end
self._commit(victim, old_path)
victim.rip_count = rips
return False
def _build_connections(self) -> list[_Connection]:
pins_by_net = self._pins_by_net()
zones_by_net: dict[int, list] = {}
@@ -1189,6 +1228,7 @@ def route_board(
seed: int | None = None,
route_first: frozenset | None = None,
access_cost: float = 0.0,
rollback: bool = False,
) -> RouteResult:
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
router = GridRouter(
@@ -1207,5 +1247,6 @@ def route_board(
seed=seed,
route_first=route_first,
access_cost=access_cost,
rollback=rollback,
)
return router.route()