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:
parent
ab786b2c5c
commit
d44275f07b
@ -1,20 +1,27 @@
|
|||||||
"""A grid-based maze router (MVP), now multi-layer with vias.
|
"""A grid-based maze router (MVP): multi-layer, vias, and rip-up-and-retry.
|
||||||
|
|
||||||
Still an *MVP* — not a port of FreeRouting's expansion-room maze — but it routes
|
Still an *MVP* — not a port of FreeRouting's expansion-room maze / ripper — but
|
||||||
across the board's signal layers and changes layers through vias when a net
|
it routes across the board's signal layers, changes layers through vias, and
|
||||||
cannot get through on one layer:
|
recovers from bad greedy net orderings with rip-up-and-retry:
|
||||||
|
|
||||||
* the occupancy grid has a layer axis; A* nodes are ``(col, row, layer)``;
|
* the occupancy grid has a layer axis; A* nodes are ``(col, row, layer)``;
|
||||||
* in-plane moves (8-connected, 45-degree) cost distance and stay on a layer;
|
* in-plane moves (8-connected, 45-degree) cost distance; a **via move** changes
|
||||||
* a **via move** transitions between layers at a cell for a configurable
|
layer at a cell for a configurable ``via_cost``; a through via must be clear on
|
||||||
``via_cost`` (so the router prefers one layer but will change to get through);
|
every signal layer;
|
||||||
* a cell is blocked per-layer by other-net pads/traces/vias on that layer plus
|
* occupancy is tracked *per connection* so a specific routed connection can be
|
||||||
keepouts; a via cell must be clear on **all** signal layers (a through via);
|
removed. When a connection cannot reach its target through free space, the
|
||||||
* each net's ratsnest is connected pin-to-pin; routed traces and vias then block
|
router runs a **rip-up** search that may pass through other nets' traces (at a
|
||||||
other nets. Trace endpoints land exactly on the connected pads.
|
penalty), rips up the connections it crosses, routes the failing connection,
|
||||||
|
and re-queues the ripped connections for a later pass;
|
||||||
|
* 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 penalty to rip a connection escalates with its rip count, so
|
||||||
|
repeatedly-ripped connections harden into walls. Everything is deterministic
|
||||||
|
(no RNG).
|
||||||
|
|
||||||
Deferred (still): FreeRouting's free-space expansion rooms, rip-up-and-retry,
|
Deferred (still): FreeRouting's free-space expansion rooms, exact shove /
|
||||||
blind/buried via spans, and shove. Those raise coverage/quality on dense boards.
|
DRC-clean geometry, and blind/buried via spans.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -52,12 +59,14 @@ class RouteResult:
|
|||||||
``routed_net_numbers`` are the nets that got any wire or via.
|
``routed_net_numbers`` are the nets that got any wire or via.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("wires", "vias", "half_width")
|
__slots__ = ("wires", "vias", "half_width", "unrouted")
|
||||||
|
|
||||||
def __init__(self, half_width: int) -> None:
|
def __init__(self, half_width: int) -> None:
|
||||||
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
|
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
|
||||||
self.vias: dict[int, list[IntPoint]] = {}
|
self.vias: dict[int, list[IntPoint]] = {}
|
||||||
self.half_width = half_width
|
self.half_width = half_width
|
||||||
|
#: number of ratsnest connections left unrouted
|
||||||
|
self.unrouted = 0
|
||||||
|
|
||||||
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
|
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
|
||||||
self.wires.setdefault(net_no, []).append((layer, path))
|
self.wires.setdefault(net_no, []).append((layer, path))
|
||||||
@ -75,8 +84,37 @@ class RouteResult:
|
|||||||
return sum(len(v) for v in self.vias.values())
|
return sum(len(v) for v in self.vias.values())
|
||||||
|
|
||||||
|
|
||||||
|
class _Connection:
|
||||||
|
"""One ratsnest connection (a pin pair) and its routed state."""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
"index",
|
||||||
|
"net_no",
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"routed",
|
||||||
|
"rip_count",
|
||||||
|
"wires",
|
||||||
|
"vias",
|
||||||
|
"trace_cells",
|
||||||
|
"via_cells",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
||||||
|
self.index = index
|
||||||
|
self.net_no = net_no
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
self.routed = False
|
||||||
|
self.rip_count = 0
|
||||||
|
self.wires: list[tuple[int, list[IntPoint]]] = []
|
||||||
|
self.vias: list[IntPoint] = []
|
||||||
|
self.trace_cells: list[tuple[Cell, int]] = []
|
||||||
|
self.via_cells: list[Cell] = []
|
||||||
|
|
||||||
|
|
||||||
class GridRouter:
|
class GridRouter:
|
||||||
"""Routes a :class:`BasicBoard` across signal layers with a grid A* + vias."""
|
"""Routes a :class:`BasicBoard` with a grid A*, vias, and rip-up-and-retry."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -86,12 +124,20 @@ class GridRouter:
|
|||||||
clearance: int,
|
clearance: int,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
via_cost: float = 10.0,
|
via_cost: float = 10.0,
|
||||||
|
rip_up: bool = True,
|
||||||
|
max_passes: int = 10,
|
||||||
|
rip_cap: int = 4,
|
||||||
|
rip_penalty: float = 30.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.board = board
|
self.board = board
|
||||||
self.trace_width = max(trace_width, 1)
|
self.trace_width = max(trace_width, 1)
|
||||||
self.clearance = max(clearance, 0)
|
self.clearance = max(clearance, 0)
|
||||||
self.half_width = self.trace_width // 2
|
self.half_width = self.trace_width // 2
|
||||||
self.via_cost = via_cost
|
self.via_cost = via_cost
|
||||||
|
self.rip_up = rip_up
|
||||||
|
self.max_passes = max(max_passes, 1)
|
||||||
|
self.rip_cap = max(rip_cap, 0)
|
||||||
|
self.rip_penalty = rip_penalty
|
||||||
# cell size must fit a trace plus its clearance to a neighbouring trace
|
# cell size must fit a trace plus its clearance to a neighbouring trace
|
||||||
self.step = max(self.trace_width + self.clearance, 1)
|
self.step = max(self.trace_width + self.clearance, 1)
|
||||||
|
|
||||||
@ -105,11 +151,12 @@ class GridRouter:
|
|||||||
self.cols = max(1, math.ceil((box.ur.x - box.ll.x) / self.step) + 1)
|
self.cols = max(1, math.ceil((box.ur.x - box.ll.x) / self.step) + 1)
|
||||||
self.rows = max(1, math.ceil((box.ur.y - box.ll.y) / self.step) + 1)
|
self.rows = max(1, math.ceil((box.ur.y - box.ll.y) / self.step) + 1)
|
||||||
|
|
||||||
# per-(cell, layer) occupancy
|
# static obstacles
|
||||||
self._pad_block: dict[Node, set[int]] = {}
|
self._pad_block: dict[Node, set[int]] = {}
|
||||||
self._keepout: set[Node] = set()
|
self._keepout: set[Node] = set()
|
||||||
self._trace_block: dict[Node, int] = {}
|
# per-connection occupancy (owner = connection index)
|
||||||
self._via_block: dict[Cell, int] = {}
|
self._cell_owner: dict[Node, int] = {}
|
||||||
|
self._via_owner: dict[Cell, int] = {}
|
||||||
self._rasterize_obstacles()
|
self._rasterize_obstacles()
|
||||||
|
|
||||||
# --- grid helpers -------------------------------------------------------
|
# --- grid helpers -------------------------------------------------------
|
||||||
@ -154,34 +201,68 @@ class GridRouter:
|
|||||||
for cell in self._cells_in_box(box, self.clearance):
|
for cell in self._cells_in_box(box, self.clearance):
|
||||||
self._keepout.add((*cell, item.layer))
|
self._keepout.add((*cell, item.layer))
|
||||||
|
|
||||||
def _blocked(self, cell: Cell, layer: int, net_no: int) -> bool:
|
# --- occupancy cost -----------------------------------------------------
|
||||||
|
|
||||||
|
def _enter_cost(
|
||||||
|
self, cell: Cell, layer: int, net_no: int, allow_ripup: bool, conns: list[_Connection]
|
||||||
|
) -> float | None:
|
||||||
|
"""Cost to enter ``(cell, layer)`` for ``net_no``; ``None`` if blocked.
|
||||||
|
|
||||||
|
Static pads/keepouts always block. A cell occupied by another net's trace
|
||||||
|
or via is impassable in free-space mode; in rip-up mode it is passable at
|
||||||
|
a penalty that escalates with the owner's rip count, unless the owner has
|
||||||
|
already hit ``rip_cap`` (then it hardens into a wall).
|
||||||
|
"""
|
||||||
node = (*cell, layer)
|
node = (*cell, layer)
|
||||||
if node in self._keepout:
|
if node in self._keepout:
|
||||||
return True
|
return None
|
||||||
pads = self._pad_block.get(node)
|
pads = self._pad_block.get(node)
|
||||||
if pads and any(n != net_no for n in pads):
|
if pads and any(n != net_no for n in pads):
|
||||||
return True
|
return None
|
||||||
trace = self._trace_block.get(node)
|
blocking = None
|
||||||
if trace is not None and trace != net_no:
|
owner = self._cell_owner.get(node)
|
||||||
return True
|
if owner is not None and conns[owner].net_no != net_no:
|
||||||
via = self._via_block.get(cell)
|
blocking = owner
|
||||||
return via is not None and via != net_no
|
else:
|
||||||
|
via_owner = self._via_owner.get(cell)
|
||||||
|
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||||
|
blocking = via_owner
|
||||||
|
if blocking is None:
|
||||||
|
return 0.0
|
||||||
|
if not allow_ripup:
|
||||||
|
return None
|
||||||
|
rip_count = conns[blocking].rip_count
|
||||||
|
if rip_count >= self.rip_cap:
|
||||||
|
return None
|
||||||
|
return self.rip_penalty * (1 + rip_count)
|
||||||
|
|
||||||
def _via_placeable(self, cell: Cell, net_no: int) -> bool:
|
def _via_placeable(self, cell: Cell, net_no: int, conns: list[_Connection]) -> bool:
|
||||||
"""A through via at ``cell`` needs every signal layer clear of other nets."""
|
"""A through via needs every signal layer strictly free of other nets."""
|
||||||
return all(not self._blocked(cell, layer, net_no) for layer in self.layers)
|
return all(
|
||||||
|
self._enter_cost(cell, layer, net_no, False, conns) == 0.0 for layer in self.layers
|
||||||
|
)
|
||||||
|
|
||||||
# --- A* search ----------------------------------------------------------
|
# --- A* search ----------------------------------------------------------
|
||||||
|
|
||||||
def _search(
|
def _search(
|
||||||
self, starts: set[Node], goal_cell: Cell, goals: set[Node], net_no: int
|
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
|
||||||
) -> list[Node] | None:
|
) -> list[Node] | None:
|
||||||
|
start_cell = self._cell_of(conn.a.location)
|
||||||
|
goal_cell = self._cell_of(conn.b.location)
|
||||||
|
a_layers = [layer for layer in conn.a.layers if layer in self.layers]
|
||||||
|
b_layers = [layer for layer in conn.b.layers if layer in self.layers]
|
||||||
|
if not a_layers or not b_layers:
|
||||||
|
return None
|
||||||
|
starts = {(*start_cell, layer) for layer in a_layers}
|
||||||
|
goals = {(*goal_cell, layer) for layer in b_layers}
|
||||||
if starts & goals:
|
if starts & goals:
|
||||||
return [next(iter(starts & goals))]
|
return [min(starts & goals)]
|
||||||
|
net_no = conn.net_no
|
||||||
|
|
||||||
open_heap: list[tuple[float, Node]] = []
|
open_heap: list[tuple[float, Node]] = []
|
||||||
g_score: dict[Node, float] = {}
|
g_score: dict[Node, float] = {}
|
||||||
came_from: dict[Node, Node] = {}
|
came_from: dict[Node, Node] = {}
|
||||||
for s in starts:
|
for s in sorted(starts):
|
||||||
g_score[s] = 0.0
|
g_score[s] = 0.0
|
||||||
heapq.heappush(open_heap, (self._h(s, goal_cell), s))
|
heapq.heappush(open_heap, (self._h(s, goal_cell), s))
|
||||||
|
|
||||||
@ -191,30 +272,28 @@ class GridRouter:
|
|||||||
return _reconstruct(came_from, current)
|
return _reconstruct(came_from, current)
|
||||||
cx, cy, cl = current
|
cx, cy, cl = current
|
||||||
base = g_score[current]
|
base = g_score[current]
|
||||||
# in-plane moves
|
|
||||||
for dx, dy in _NEIGHBOURS:
|
for dx, dy in _NEIGHBOURS:
|
||||||
cell = (cx + dx, cy + dy)
|
cell = (cx + dx, cy + dy)
|
||||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||||
continue
|
continue
|
||||||
nxt = (*cell, cl)
|
nxt = (*cell, cl)
|
||||||
if nxt not in goals and self._blocked(cell, cl, net_no):
|
if nxt in goals:
|
||||||
continue
|
enter = 0.0
|
||||||
cost = 1.0 if dx == 0 or dy == 0 else math.sqrt(2)
|
else:
|
||||||
self._relax(current, nxt, base + cost, goal_cell, g_score, came_from, open_heap)
|
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||||
# via moves (change layer at the same cell)
|
if enter is None:
|
||||||
if len(self.layers) > 1 and self._via_placeable((cx, cy), net_no):
|
continue
|
||||||
|
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 and self._via_placeable((cx, cy), net_no, conns):
|
||||||
for layer in self.layers:
|
for layer in self.layers:
|
||||||
if layer == cl:
|
if layer == cl:
|
||||||
continue
|
continue
|
||||||
nxt = (cx, cy, layer)
|
nxt = (cx, cy, layer)
|
||||||
self._relax(
|
self._relax(
|
||||||
current,
|
current, nxt, base + self.via_cost, goal_cell, g_score, came_from, open_heap
|
||||||
nxt,
|
|
||||||
base + self.via_cost,
|
|
||||||
goal_cell,
|
|
||||||
g_score,
|
|
||||||
came_from,
|
|
||||||
open_heap,
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -227,28 +306,106 @@ class GridRouter:
|
|||||||
def _h(self, node: Node, goal_cell: Cell) -> float:
|
def _h(self, node: Node, goal_cell: Cell) -> float:
|
||||||
return math.hypot(node[0] - goal_cell[0], node[1] - goal_cell[1])
|
return math.hypot(node[0] - goal_cell[0], node[1] - goal_cell[1])
|
||||||
|
|
||||||
# --- commit / marking ---------------------------------------------------
|
# --- commit / rip -------------------------------------------------------
|
||||||
|
|
||||||
def _mark(self, wires, vias, net_no: int) -> None:
|
def _commit(self, conn: _Connection, path: list[Node]) -> None:
|
||||||
for layer, cells in wires:
|
wire_segs, via_cells = _split_path(path)
|
||||||
for cell in cells:
|
conn.trace_cells = [(cell, layer) for layer, cells in wire_segs for cell in cells]
|
||||||
self._trace_block[(*cell, layer)] = net_no
|
conn.via_cells = list(via_cells)
|
||||||
for cell in vias:
|
for cell, layer in conn.trace_cells:
|
||||||
self._via_block[cell] = net_no
|
self._cell_owner[(*cell, layer)] = conn.index
|
||||||
|
for cell in conn.via_cells:
|
||||||
|
self._via_owner[cell] = conn.index
|
||||||
|
conn.wires = []
|
||||||
|
for idx, (layer, cells) in enumerate(wire_segs):
|
||||||
|
points = [self._cell_center(c) for c in cells]
|
||||||
|
if idx == 0:
|
||||||
|
points.insert(0, conn.a.location)
|
||||||
|
if idx == len(wire_segs) - 1:
|
||||||
|
points.append(conn.b.location)
|
||||||
|
points = _simplify(points)
|
||||||
|
if len(points) >= 2:
|
||||||
|
conn.wires.append((layer, points))
|
||||||
|
conn.vias = [self._cell_center(c) for c in conn.via_cells]
|
||||||
|
conn.routed = True
|
||||||
|
|
||||||
# --- public routing -----------------------------------------------------
|
def _rip(self, conn: _Connection) -> None:
|
||||||
|
for cell, layer in conn.trace_cells:
|
||||||
|
if self._cell_owner.get((*cell, layer)) == conn.index:
|
||||||
|
del self._cell_owner[(*cell, layer)]
|
||||||
|
for cell in conn.via_cells:
|
||||||
|
if self._via_owner.get(cell) == conn.index:
|
||||||
|
del self._via_owner[cell]
|
||||||
|
conn.trace_cells = []
|
||||||
|
conn.via_cells = []
|
||||||
|
conn.wires = []
|
||||||
|
conn.vias = []
|
||||||
|
conn.routed = False
|
||||||
|
conn.rip_count += 1
|
||||||
|
|
||||||
|
def _crossed_owners(self, path: list[Node], net_no: int, conns: list[_Connection]) -> set[int]:
|
||||||
|
owners: set[int] = set()
|
||||||
|
for cx, cy, cl in path:
|
||||||
|
owner = self._cell_owner.get((cx, cy, cl))
|
||||||
|
if owner is not None and conns[owner].net_no != net_no:
|
||||||
|
owners.add(owner)
|
||||||
|
via_owner = self._via_owner.get((cx, cy))
|
||||||
|
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||||
|
owners.add(via_owner)
|
||||||
|
return owners
|
||||||
|
|
||||||
|
# --- passes -------------------------------------------------------------
|
||||||
|
|
||||||
def route(self) -> RouteResult:
|
def route(self) -> RouteResult:
|
||||||
"""Route every net with >= 2 pins across the router's signal layers."""
|
"""Route all ratsnest connections, using rip-up-and-retry across passes."""
|
||||||
result = RouteResult(self.half_width)
|
conns = self._build_connections()
|
||||||
|
if not conns:
|
||||||
|
return RouteResult(self.half_width)
|
||||||
|
|
||||||
|
passes = self.max_passes if self.rip_up else 1
|
||||||
|
best_count = -1
|
||||||
|
best_snapshot: list[tuple[int, list, list]] | None = None
|
||||||
|
best_unrouted = len(conns)
|
||||||
|
for _ in range(passes):
|
||||||
|
changed = False
|
||||||
|
for conn in conns: # deterministic: connection creation order
|
||||||
|
if conn.routed:
|
||||||
|
continue
|
||||||
|
if self._try_route(conn, conns):
|
||||||
|
changed = True
|
||||||
|
routed_count = sum(1 for c in conns if c.routed)
|
||||||
|
if routed_count > best_count:
|
||||||
|
best_count = routed_count
|
||||||
|
best_unrouted = len(conns) - routed_count
|
||||||
|
best_snapshot = self._snapshot(conns)
|
||||||
|
if routed_count == len(conns):
|
||||||
|
break
|
||||||
|
if not changed:
|
||||||
|
break
|
||||||
|
return self._result_from(best_snapshot or [], best_unrouted)
|
||||||
|
|
||||||
|
def _try_route(self, conn: _Connection, conns: list[_Connection]) -> bool:
|
||||||
|
path = self._search(conn, allow_ripup=False, conns=conns)
|
||||||
|
if path is None:
|
||||||
|
if not self.rip_up:
|
||||||
|
return False
|
||||||
|
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])
|
||||||
|
self._commit(conn, path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _build_connections(self) -> list[_Connection]:
|
||||||
pins_by_net = self._pins_by_net()
|
pins_by_net = self._pins_by_net()
|
||||||
for net_no in sorted(pins_by_net, key=lambda n: len(pins_by_net[n])):
|
conns: list[_Connection] = []
|
||||||
|
# deterministic: fewer-pin nets first, then by net number
|
||||||
|
for net_no in sorted(pins_by_net, key=lambda n: (len(pins_by_net[n]), n)):
|
||||||
pins = pins_by_net[net_no]
|
pins = pins_by_net[net_no]
|
||||||
if len(pins) < 2:
|
|
||||||
continue
|
|
||||||
for a, b in zip(pins, pins[1:], strict=False):
|
for a, b in zip(pins, pins[1:], strict=False):
|
||||||
self._route_connection(a, b, net_no, result)
|
conns.append(_Connection(len(conns), net_no, a, b))
|
||||||
return result
|
return conns
|
||||||
|
|
||||||
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
||||||
by_net: dict[int, list[Pin]] = {}
|
by_net: dict[int, list[Pin]] = {}
|
||||||
@ -259,32 +416,22 @@ class GridRouter:
|
|||||||
by_net.setdefault(net_no, []).append(pin)
|
by_net.setdefault(net_no, []).append(pin)
|
||||||
return by_net
|
return by_net
|
||||||
|
|
||||||
def _route_connection(self, a: Pin, b: Pin, net_no: int, result: RouteResult) -> None:
|
def _snapshot(self, conns: list[_Connection]) -> list[tuple[int, list, list]]:
|
||||||
start_cell = self._cell_of(a.location)
|
return [
|
||||||
goal_cell = self._cell_of(b.location)
|
(c.net_no, [(layer, list(pts)) for layer, pts in c.wires], list(c.vias))
|
||||||
a_layers = [layer for layer in a.layers if layer in self.layers]
|
for c in conns
|
||||||
b_layers = [layer for layer in b.layers if layer in self.layers]
|
if c.routed
|
||||||
if not a_layers or not b_layers:
|
]
|
||||||
return
|
|
||||||
starts = {(*start_cell, layer) for layer in a_layers}
|
def _result_from(self, snapshot, unrouted: int) -> RouteResult:
|
||||||
goals = {(*goal_cell, layer) for layer in b_layers}
|
result = RouteResult(self.half_width)
|
||||||
path = self._search(starts, goal_cell, goals, net_no)
|
result.unrouted = unrouted
|
||||||
if path is None:
|
for net_no, wires, vias in snapshot:
|
||||||
return
|
for layer, points in wires:
|
||||||
wires, vias = _split_path(path)
|
|
||||||
self._mark(wires, vias, net_no)
|
|
||||||
# emit wires with exact pad endpoints; emit vias at their cell centres
|
|
||||||
for idx, (layer, cells) in enumerate(wires):
|
|
||||||
points = [self._cell_center(c) for c in cells]
|
|
||||||
if idx == 0:
|
|
||||||
points.insert(0, a.location)
|
|
||||||
if idx == len(wires) - 1:
|
|
||||||
points.append(b.location)
|
|
||||||
points = _simplify(points)
|
|
||||||
if len(points) >= 2:
|
|
||||||
result.add_wire(net_no, layer, points)
|
result.add_wire(net_no, layer, points)
|
||||||
for cell in vias:
|
for location in vias:
|
||||||
result.add_via(net_no, self._cell_center(cell))
|
result.add_via(net_no, location)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _split_path(path: list[Node]) -> tuple[list[tuple[int, list[Cell]]], list[Cell]]:
|
def _split_path(path: list[Node]) -> tuple[list[tuple[int, list[Cell]]], list[Cell]]:
|
||||||
@ -336,6 +483,8 @@ def route_board(
|
|||||||
clearance: int,
|
clearance: int,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
via_cost: float = 10.0,
|
via_cost: float = 10.0,
|
||||||
|
rip_up: bool = True,
|
||||||
|
max_passes: int = 10,
|
||||||
) -> RouteResult:
|
) -> RouteResult:
|
||||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||||
router = GridRouter(
|
router = GridRouter(
|
||||||
@ -344,5 +493,7 @@ def route_board(
|
|||||||
clearance=clearance,
|
clearance=clearance,
|
||||||
layers=layers,
|
layers=layers,
|
||||||
via_cost=via_cost,
|
via_cost=via_cost,
|
||||||
|
rip_up=rip_up,
|
||||||
|
max_passes=max_passes,
|
||||||
)
|
)
|
||||||
return router.route()
|
return router.route()
|
||||||
|
|||||||
@ -44,23 +44,37 @@ def _via_padstack(dsn: DsnBoard) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def route_dsn_board(
|
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]]:
|
) -> tuple[RouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
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)
|
board = build_board(dsn)
|
||||||
scale = max(dsn.resolution.value, 1)
|
scale = max(dsn.resolution.value, 1)
|
||||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||||
clearance_board = round(_rule_clearance_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]
|
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
|
"""Route ``dsn`` and convert the board-unit paths + vias to a DSN-unit
|
||||||
:class:`~freeroute.ses.RoutingResult` for SES emission."""
|
: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)
|
width_dsn = _rule_width_dsn(dsn)
|
||||||
via_name = _via_padstack(dsn)
|
via_name = _via_padstack(dsn)
|
||||||
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user