diff --git a/src/freeroute/route/grid_router.py b/src/freeroute/route/grid_router.py index 23b963a..1295aab 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/freeroute/route/grid_router.py @@ -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