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
|
# orthogonal routing (axis-aligned segments only) yields exact IntBox
|
||||||
# trace copper; 8-connected also produces 45-degree segments.
|
# trace copper; 8-connected also produces 45-degree segments.
|
||||||
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
||||||
|
self._orthogonal = orthogonal
|
||||||
# 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)
|
||||||
# A via's copper is wider than a trace. Another net's trace needs its
|
# A via's copper is wider than a trace. Another net's trace needs its
|
||||||
@@ -332,14 +333,11 @@ class GridRouter:
|
|||||||
def _search(
|
def _search(
|
||||||
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
|
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)
|
goal_cell = self._cell_of(conn.b.location)
|
||||||
a_layers = [layer for layer in conn.a.layers if layer in self.layers]
|
starts = self._end_nodes(conn.a, conn.net_no, conns)
|
||||||
b_layers = [layer for layer in conn.b.layers if layer in self.layers]
|
goals = self._end_nodes(conn.b, conn.net_no, conns)
|
||||||
if not a_layers or not b_layers:
|
if not starts or not goals:
|
||||||
return None
|
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 [min(starts & goals)]
|
return [min(starts & goals)]
|
||||||
net_no = conn.net_no
|
net_no = conn.net_no
|
||||||
@@ -347,9 +345,11 @@ class GridRouter:
|
|||||||
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] = {}
|
||||||
|
# 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):
|
for s in sorted(starts):
|
||||||
g_score[s] = 0.0
|
g_score[s] = self._stub_cost(conn.a, s)
|
||||||
heapq.heappush(open_heap, (self._h(s, goal_cell), s))
|
heapq.heappush(open_heap, (g_score[s] + self._h(s, goal_cell), s))
|
||||||
|
|
||||||
while open_heap:
|
while open_heap:
|
||||||
_, current = heapq.heappop(open_heap)
|
_, current = heapq.heappop(open_heap)
|
||||||
@@ -363,7 +363,7 @@ class GridRouter:
|
|||||||
continue
|
continue
|
||||||
nxt = (*cell, cl)
|
nxt = (*cell, cl)
|
||||||
if nxt in goals:
|
if nxt in goals:
|
||||||
enter = 0.0
|
enter = goal_stub[nxt]
|
||||||
else:
|
else:
|
||||||
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||||
if enter is None:
|
if enter is None:
|
||||||
@@ -382,6 +382,65 @@ class GridRouter:
|
|||||||
)
|
)
|
||||||
return None
|
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):
|
def _relax(self, current, nxt, tentative, goal_cell, g_score, came_from, open_heap):
|
||||||
if tentative < g_score.get(nxt, math.inf):
|
if tentative < g_score.get(nxt, math.inf):
|
||||||
came_from[nxt] = current
|
came_from[nxt] = current
|
||||||
@@ -405,6 +464,13 @@ class GridRouter:
|
|||||||
if c not in self._via_owner:
|
if c not in self._via_owner:
|
||||||
self._via_owner[c] = conn.index
|
self._via_owner[c] = conn.index
|
||||||
conn.via_halo.append(c)
|
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 = []
|
conn.wires = []
|
||||||
for idx, (layer, cells) in enumerate(wire_segs):
|
for idx, (layer, cells) in enumerate(wire_segs):
|
||||||
points = [self._cell_center(c) for c in cells]
|
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