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:
2026-09-27 13:01:27 -06:00
parent cdb3c7b568
commit 3253e6fa43
+103
View File
@@ -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