Add rip-up-and-retry to the maze router

Restructures the router so occupancy is tracked per connection, then adds
a rip-up-and-retry loop that recovers from bad greedy net orderings.

When a connection cannot reach its target through free space, a rip-up
search may pass through other nets' traces at an escalating penalty; the
router rips up the connections that path crosses (fully removing their
occupancy), routes the failing connection, and re-queues the ripped
connections. Passes iterate up to max_passes, keeping the best
(fewest-unrouted) result and stopping on full success or when a pass
changes nothing.

Thrash prevention: each connection may be ripped at most rip_cap times,
and the rip penalty escalates with a connection's rip count, so
repeatedly-ripped connections harden into walls. Fully deterministic
(sorted rip sets, connection-creation order, tuple-keyed A* — no RNG).

route()/route_dsn_board()/route_board() take rip_up and max_passes;
rip_up=False reproduces the previous greedy single-pass behaviour for
comparison. RouteResult now reports the unrouted connection count.
This commit is contained in:
2026-07-12 13:38:40 -06:00
parent ab786b2c5c
commit d44275f07b
2 changed files with 256 additions and 91 deletions
+19 -5
View File
@@ -44,23 +44,37 @@ def _via_padstack(dsn: DsnBoard) -> str:
def route_dsn_board(
dsn: DsnBoard, *, layers: list[int] | None = None
dsn: DsnBoard,
*,
layers: list[int] | None = None,
rip_up: bool = True,
max_passes: int = 10,
) -> 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
indices (default: all signal layers, i.e. multi-layer with vias)."""
indices (default: all signal layers, i.e. multi-layer with vias); ``rip_up``
toggles rip-up-and-retry (``False`` = greedy single pass)."""
board = build_board(dsn)
scale = max(dsn.resolution.value, 1)
width_board = round(_rule_width_dsn(dsn) * scale)
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
result = route_board(board, trace_width=width_board, clearance=clearance_board, layers=layers)
result = route_board(
board,
trace_width=width_board,
clearance=clearance_board,
layers=layers,
rip_up=rip_up,
max_passes=max_passes,
)
return result, scale, [layer.name for layer in dsn.layers]
def build_routing_result(dsn: DsnBoard, *, layers: list[int] | None = None) -> RoutingResult:
def build_routing_result(
dsn: DsnBoard, *, layers: list[int] | None = None, rip_up: bool = True
) -> RoutingResult:
"""Route ``dsn`` and convert the board-unit paths + vias to a DSN-unit
:class:`~freeroute.ses.RoutingResult` for SES emission."""
route, scale, layer_names = route_dsn_board(dsn, layers=layers)
route, scale, layer_names = route_dsn_board(dsn, layers=layers, rip_up=rip_up)
width_dsn = _rule_width_dsn(dsn)
via_name = _via_padstack(dsn)
# net_number is assigned in DSN order by build_board, so index i -> number i+1