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:
2026-09-27 07:37:50 -06:00
parent c1df284147
commit 9fa5432953
2 changed files with 141 additions and 9 deletions
+66
View File
@@ -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