Give pin stubs clearance, and let a route leave a pin by any open side
Every route starts and ends with a stub from the pin to its first grid cell, and the grid never saw that copper: on the rc4 sense board 24 of 39 connections the exact verifier rejected had a later route pass beside an earlier stub. A route's stubs now claim the cells they come near, a stub must be clear of other nets to be used, and a route may start from any enterable neighbour of the pin cell, costed by stub length (Manhattan on the orthogonal track, so a pin on a cell centre still leaves straight). Tests: a route near a rounded corner keeps its edge clearance; and a strict xfail that pins down the remaining gap, a 0.5 mm pin row the grid cannot give a column to every pin of.
This commit is contained in:
@@ -148,6 +148,7 @@ class GridRouter:
|
||||
# orthogonal routing (axis-aligned segments only) yields exact IntBox
|
||||
# trace copper; 8-connected also produces 45-degree segments.
|
||||
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
||||
self._orthogonal = orthogonal
|
||||
# cell size must fit a trace plus its clearance to a neighbouring trace
|
||||
self.step = max(self.trace_width + self.clearance, 1)
|
||||
# A via's copper is wider than a trace. Another net's trace needs its
|
||||
@@ -332,14 +333,11 @@ class GridRouter:
|
||||
def _search(
|
||||
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
|
||||
) -> 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:
|
||||
starts = self._end_nodes(conn.a, conn.net_no, conns)
|
||||
goals = self._end_nodes(conn.b, conn.net_no, conns)
|
||||
if not starts or not goals:
|
||||
return None
|
||||
starts = {(*start_cell, layer) for layer in a_layers}
|
||||
goals = {(*goal_cell, layer) for layer in b_layers}
|
||||
if starts & goals:
|
||||
return [min(starts & goals)]
|
||||
net_no = conn.net_no
|
||||
@@ -347,9 +345,11 @@ class GridRouter:
|
||||
open_heap: list[tuple[float, Node]] = []
|
||||
g_score: dict[Node, float] = {}
|
||||
came_from: dict[Node, Node] = {}
|
||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
||||
goal_stub = {n: self._stub_cost(conn.b, n) for n in goals}
|
||||
for s in sorted(starts):
|
||||
g_score[s] = 0.0
|
||||
heapq.heappush(open_heap, (self._h(s, goal_cell), s))
|
||||
g_score[s] = self._stub_cost(conn.a, s)
|
||||
heapq.heappush(open_heap, (g_score[s] + self._h(s, goal_cell), s))
|
||||
|
||||
while open_heap:
|
||||
_, current = heapq.heappop(open_heap)
|
||||
@@ -363,7 +363,7 @@ class GridRouter:
|
||||
continue
|
||||
nxt = (*cell, cl)
|
||||
if nxt in goals:
|
||||
enter = 0.0
|
||||
enter = goal_stub[nxt]
|
||||
else:
|
||||
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||
if enter is None:
|
||||
@@ -382,6 +382,65 @@ class GridRouter:
|
||||
)
|
||||
return None
|
||||
|
||||
# --- pin stubs ----------------------------------------------------------
|
||||
# A pin rarely sits on a cell centre, so every route starts and ends with a
|
||||
# short stub from the pin to its first cell. The grid has to see that
|
||||
# copper too, or a stub ends up closer than clearance to a trace in the
|
||||
# cell beside it.
|
||||
|
||||
def _stub_cells(self, pin_location: IntPoint, cell: Cell) -> list[Cell]:
|
||||
"""Cells where another net's trace would come too close to the stub."""
|
||||
from freeroute.board.outline import _point_segment
|
||||
|
||||
end = self._cell_center(cell)
|
||||
reach = 2 * self.half_width + self.clearance
|
||||
box = IntBox(
|
||||
min(pin_location.x, end.x), min(pin_location.y, end.y),
|
||||
max(pin_location.x, end.x), max(pin_location.y, end.y),
|
||||
)
|
||||
return [
|
||||
c
|
||||
for c in self._cells_in_box(box, reach)
|
||||
if _point_segment(self._cell_center(c), pin_location, end) < reach
|
||||
]
|
||||
|
||||
def _stub_clear(self, pin: Pin, cell: Cell, layer: int, net_no: int, conns) -> bool:
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
owner = self._cell_owner.get((*c, layer))
|
||||
if owner is not None and conns[owner].net_no != net_no:
|
||||
return False
|
||||
via_owner = self._via_owner.get(c)
|
||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _stub_cost(self, pin: Pin, node: Node) -> float:
|
||||
c = self._cell_center(node[:2])
|
||||
dx, dy = abs(c.x - pin.location.x), abs(c.y - pin.location.y)
|
||||
if self._orthogonal: # measured the way the route itself is allowed to move
|
||||
return (dx + dy) / self.step
|
||||
return math.hypot(dx, dy) / self.step
|
||||
|
||||
def _end_nodes(self, pin: Pin, net_no: int, conns) -> set[Node]:
|
||||
"""Nodes a route may start or end on for ``pin``: the nearest cell and
|
||||
its neighbours, where the cell is enterable and the stub to it is clear.
|
||||
A crowded fine-pitch pin gets more than one way out."""
|
||||
layers = [layer for layer in pin.layers if layer in self.layers]
|
||||
cx, cy = self._cell_of(pin.location)
|
||||
nodes: set[Node] = set()
|
||||
for dx, dy in ((0, 0), *self._neighbours):
|
||||
cell = (cx + dx, cy + dy)
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
continue
|
||||
for layer in layers:
|
||||
if (dx or dy) and self._enter_cost(cell, layer, net_no, False, conns) is None:
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
nodes.add((*cell, layer))
|
||||
if not nodes: # nothing clear: fall back to the nearest cell, as before
|
||||
nodes = {(cx, cy, layer) for layer in layers}
|
||||
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
|
||||
@@ -405,6 +464,13 @@ class GridRouter:
|
||||
if c not in self._via_owner:
|
||||
self._via_owner[c] = conn.index
|
||||
conn.via_halo.append(c)
|
||||
for pin, (layer, cells) in ((conn.a, wire_segs[0]), (conn.b, wire_segs[-1])) if wire_segs else ():
|
||||
cell = cells[0] if pin is conn.a else cells[-1]
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
node = (*c, layer)
|
||||
if node not in self._cell_owner:
|
||||
self._cell_owner[node] = conn.index
|
||||
conn.trace_cells.append((c, layer))
|
||||
conn.wires = []
|
||||
for idx, (layer, cells) in enumerate(wire_segs):
|
||||
points = [self._cell_center(c) for c in cells]
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Routing behaviours found on the TerraHawk rc4 boards (2026-09-27)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.route.pipeline import route_dsn_board_exact
|
||||
|
||||
# 20 x 20 mm board whose corners are rounded with a 3 mm radius, as polylines
|
||||
_R = 3000.0
|
||||
_arc = [(_R - _R * math.cos(math.radians(a)), _R - _R * math.sin(math.radians(a))) for a in range(0, 91, 15)]
|
||||
|
||||
|
||||
def _outline_coords():
|
||||
pts = []
|
||||
for cx, cy, sx, sy in ((0, 0, 1, 1), (20000, 0, -1, 1), (20000, -20000, -1, -1), (0, -20000, 1, -1)):
|
||||
corner = [(cx + sx * x, cy - sy * y) for x, y in _arc]
|
||||
pts.extend(corner if sx * sy > 0 else corner[::-1])
|
||||
return " ".join(f"{x:.1f} {y:.1f}" for x, y in pts)
|
||||
|
||||
|
||||
def _dsn(placement, network, extra=""):
|
||||
return f"""(pcb t (parser) (resolution um 10) (unit um)
|
||||
(structure (layer F.Cu (type signal)) (layer B.Cu (type signal))
|
||||
(boundary (path pcb 0 {_outline_coords()}))
|
||||
(via "Via[0-1]_600:300_um") (rule (width 250) (clearance 150)) {extra})
|
||||
(placement {placement})
|
||||
(library (image P (pin pad 1 0 0))
|
||||
(padstack pad (shape (rect F.Cu -150 -500 150 500)) (attach off))
|
||||
(padstack "Via[0-1]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off)))
|
||||
(network {network}))"""
|
||||
|
||||
|
||||
def test_routes_keep_edge_clearance_at_a_rounded_corner():
|
||||
# two pads near the top-left corner: the direct path cuts the corner arc
|
||||
text = _dsn("(component P (place A 1800 -6000 front 0) (place B 6000 -1800 front 90))",
|
||||
"(net N (pins A-1 B-1))")
|
||||
dsn = parse_dsn(text)
|
||||
result, scale, _ = route_dsn_board_exact(dsn, edge_clearance_mm=0.5)
|
||||
assert result.routed_net_numbers
|
||||
outline = build_board(dsn).outline
|
||||
half = 125 * scale
|
||||
for segs in result.result.wires.values():
|
||||
for _layer, pts in segs:
|
||||
for p in pts:
|
||||
# every trace point, plus half the trace, keeps 0.5 mm to the edge
|
||||
assert outline.edge_distance(p) >= half + 500 * scale - 1
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason="known gap: the grid step (trace + clearance, 0.4 mm here) does not line up "
|
||||
"with a 0.5 mm pin row, so only the outer pins get a column; the FreeRouting JAR, "
|
||||
"being gridless, routes all five. This is most of the gap on an LQFP.",
|
||||
)
|
||||
def test_every_pin_of_a_half_millimetre_pitch_row_escapes():
|
||||
# five pads at 0.5 mm pitch, each wired to its own pad 8 mm away
|
||||
places = " ".join(f"(place R{i} {6000 + 500 * i} -6000 front 0)" for i in range(5))
|
||||
places += " " + " ".join(f"(place F{i} {6000 + 500 * i} -14000 front 0)" for i in range(5))
|
||||
nets = " ".join(f"(net N{i} (pins R{i}-1 F{i}-1))" for i in range(5))
|
||||
result, _, _ = route_dsn_board_exact(parse_dsn(_dsn(f"(component P {places})", nets)))
|
||||
assert len(result.routed_net_numbers) == 5
|
||||
Reference in New Issue
Block a user