Escape stubs for pins walled in on the grid
On the rc5 sense board at its real 0.2 mm clearance, ten pins of the 0.5 mm pitch STM32 could only reach a pocket of seven or eight cells inside their own pads: the grid columns miss the narrow legal band past the pad tip, so twelve connections could not route even on an empty board. For a crowded pin (a neighbouring pad of another net closer than a trace can pass) whose nearby cells are such a pocket against pads and keepouts, offer straight stubs along each axis to the first open cell, accepted only if the stub clears every other net's pad by the full margin, measured exactly. All twelve then route alone; on the full board unrouted connections drop from 61 to 59, the rest being congestion.
This commit is contained in:
@@ -156,6 +156,10 @@ class GridRouter:
|
||||
heuristic_weight: float = 1.0,
|
||||
) -> None:
|
||||
self.board = board
|
||||
# how far (in cells) an escape stub may run to leave a walled-in pin
|
||||
self._escape_cells = 12
|
||||
self._crowded_cache: dict = {}
|
||||
self._trapped_cache: dict = {}
|
||||
self.heuristic_weight = max(float(heuristic_weight), 1.0)
|
||||
self.trace_width = max(trace_width, 1)
|
||||
self.clearance = max(clearance, 0)
|
||||
@@ -578,10 +582,109 @@ class GridRouter:
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
nodes.add((*cell, layer))
|
||||
# Also offer exact escape stubs: a fine-pitch pin's nearby cells can be
|
||||
# a pocket inside its own pad that leads nowhere (the grid columns miss
|
||||
# the narrow legal band past the pad tip). The stub's length is its
|
||||
# cost, so it is only taken where a short step would not do.
|
||||
if self._orthogonal and self._crowded(pin, net_no) and self._trapped(pin, net_no, nodes):
|
||||
nodes |= self._escape_nodes(pin, net_no, conns, layers, (cx, cy))
|
||||
if not nodes: # nothing clear: fall back to the nearest cell, as before
|
||||
nodes = {(cx, cy, layer) for layer in layers}
|
||||
return nodes
|
||||
|
||||
def _crowded(self, pin: Pin, net_no: int) -> bool:
|
||||
"""A pin whose neighbour pad of another net is too close for a trace
|
||||
to pass between them: a fine-pitch pin, the case escapes are for."""
|
||||
key = (id(pin), net_no)
|
||||
cached = self._crowded_cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
box = pin.shape.bounding_box() if pin.shape is not None else None
|
||||
gap = self.trace_width + 2 * self.clearance
|
||||
crowded = False
|
||||
if box is not None:
|
||||
for other in self.board.get_pins():
|
||||
if other is pin or net_no in other.net_nos or other.shape is None:
|
||||
continue
|
||||
if not set(other.layers) & set(pin.layers):
|
||||
continue
|
||||
ob = other.shape.bounding_box()
|
||||
dx = max(ob.ll.x - box.ur.x, box.ll.x - ob.ur.x, 0)
|
||||
dy = max(ob.ll.y - box.ur.y, box.ll.y - ob.ur.y, 0)
|
||||
if max(dx, dy) < gap and min(dx, dy) == 0:
|
||||
crowded = True
|
||||
break
|
||||
self._crowded_cache[key] = crowded
|
||||
return crowded
|
||||
|
||||
def _static_open(self, node: Node, net_no: int) -> bool:
|
||||
"""Open to ``net_no`` as far as pads and keepouts go (routes ignored)."""
|
||||
if node in self._keepout:
|
||||
return False
|
||||
pads = self._pad_block.get(node)
|
||||
return not (pads and any(n != net_no for n in pads) and net_no not in self._pad_own.get(node, ()))
|
||||
|
||||
def _trapped(self, pin: Pin, net_no: int, nodes: set[Node], limit: int = 60) -> bool:
|
||||
"""Whether a pin's nearby cells are a dead-end pocket, judged against
|
||||
pads and keepouts only (not other routes), so it is fixed per pin."""
|
||||
key = (id(pin), net_no)
|
||||
cached = self._trapped_cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
seen = set(nodes)
|
||||
frontier = list(nodes)
|
||||
while frontier and len(seen) < limit:
|
||||
x, y, layer = frontier.pop()
|
||||
for dx, dy in self._neighbours:
|
||||
n = (x + dx, y + dy, layer)
|
||||
if n in seen or not (0 <= n[0] < self.cols and 0 <= n[1] < self.rows):
|
||||
continue
|
||||
if self._static_open(n, net_no):
|
||||
seen.add(n)
|
||||
frontier.append(n)
|
||||
trapped = len(seen) < limit
|
||||
self._trapped_cache[key] = trapped
|
||||
return trapped
|
||||
|
||||
def _escape_nodes(self, pin: Pin, net_no: int, conns, layers, origin: Cell) -> set[Node]:
|
||||
"""For a pin walled in on the grid (a fine-pitch pin whose legal band
|
||||
the grid columns miss): look along each axis for the first open cell
|
||||
whose straight stub from the pin clears every other net's pad by the
|
||||
full margin, measured exactly rather than on the grid."""
|
||||
from freeroute.board.outline import _segment_box
|
||||
|
||||
margin = self.half_width + self.clearance
|
||||
reach = self._escape_cells
|
||||
near = [
|
||||
(p.shape.bounding_box(), set(p.layers))
|
||||
for p in self.board.get_pins()
|
||||
if p.shape is not None
|
||||
and net_no not in p.net_nos
|
||||
and abs(p.location.x - pin.location.x) < (reach + 2) * self.step * 2
|
||||
and abs(p.location.y - pin.location.y) < (reach + 2) * self.step * 2
|
||||
]
|
||||
cx, cy = origin
|
||||
nodes: set[Node] = set()
|
||||
for dx, dy in _NEIGHBOURS[:4]:
|
||||
for k in range(2, reach + 1):
|
||||
cell = (cx + dx * k, cy + dy * k)
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
break
|
||||
end = self._cell_center(cell)
|
||||
for layer in layers:
|
||||
if self._enter_cost(cell, layer, net_no, False, conns) is None:
|
||||
continue
|
||||
if any(
|
||||
layer in box_layers and _segment_box(pin.location, end, box) < margin
|
||||
for box, box_layers in near
|
||||
):
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
nodes.add((*cell, layer))
|
||||
if any(n[:2] == cell for n in nodes):
|
||||
break # the first open cell along this axis is enough
|
||||
return nodes
|
||||
|
||||
def _relax(self, current, nxt, tentative, goal_cell, g_score, came_from, open_heap):
|
||||
if tentative < g_score.get(nxt, math.inf):
|
||||
came_from[nxt] = current
|
||||
|
||||
Reference in New Issue
Block a user