The router began as a port of FreeRouting and has grown its own engine (negotiated congestion, tree nets, exact checks, axis escapes), and 'freeroute' was one letter from the project it no longer follows. The package, module and command are now rpmroute; the freeroute command stays as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a PCBench board routes to a byte-identical session either way.
202 lines
7.8 KiB
Python
202 lines
7.8 KiB
Python
"""Tests for 45-degree (diagonal) routing on the exact track.
|
|
|
|
The orthogonal exact router connects a 45-degree pin-to-pin net with a Manhattan
|
|
staircase (length ~ dx + dy). ``diagonal=True`` replaces it with a single
|
|
45-degree trace (length ~ hypot(dx, dy)) whose copper clearance is checked with
|
|
the exact integer octagon, not the coarse bounding box -- so it fits past an
|
|
obstacle that sits inside the diagonal's bounding box but clears the true thin
|
|
copper. ``diagonal_win.dsn`` places exactly such an obstacle, making the exact
|
|
clearance load-bearing rather than cosmetic.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from rpmroute.board import build_board
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.dsn.sexp import parse
|
|
from rpmroute.geometry import IntBox, IntPoint, overlaps_2d, segment_octagon
|
|
from rpmroute.route import route, route_dsn_board_exact
|
|
|
|
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
|
DIAGONAL_WIN = FIXTURES / "diagonal_win.dsn"
|
|
SIMPLE = FIXTURES / "simple_2net.dsn"
|
|
CROSSING = FIXTURES / "crossing_2net.dsn"
|
|
KICAD = FIXTURES / "kicad_routable.dsn"
|
|
|
|
|
|
def _length(result):
|
|
return sum(
|
|
math.hypot(q.x - p.x, q.y - p.y)
|
|
for _n, segs in result.result.wires.items()
|
|
for _layer, pts in segs
|
|
for p, q in zip(pts, pts[1:], strict=False)
|
|
)
|
|
|
|
|
|
def _wire_key(result):
|
|
return {
|
|
n: [(layer, [(p.x, p.y) for p in pts]) for layer, pts in segs]
|
|
for n, segs in result.result.wires.items()
|
|
}
|
|
|
|
|
|
def _pins_by_net(fixture):
|
|
board = build_board(parse_dsn(fixture.read_text()))
|
|
out: dict[int, set[tuple[int, int]]] = {}
|
|
for pin in board.get_pins():
|
|
for net_no in pin.net_nos:
|
|
out.setdefault(net_no, set()).add((pin.location.x, pin.location.y))
|
|
return out
|
|
|
|
|
|
def _rule_clearance_board(fixture):
|
|
from rpmroute.route.pipeline import _rule_clearance_dsn
|
|
|
|
dsn = parse_dsn(fixture.read_text())
|
|
return round(_rule_clearance_dsn(dsn) * max(dsn.resolution.value, 1))
|
|
|
|
|
|
# --- the falsifiable length win ----------------------------------------------
|
|
|
|
|
|
def test_orthogonal_baseline_is_manhattan_length():
|
|
"""Orthogonal (diagonal off) connects the net with a Manhattan-length route --
|
|
the staircase the exact grid produces for a 45-degree ratsnest."""
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
ortho, _, _ = route_dsn_board_exact(dsn)
|
|
assert ortho.routed_net_numbers == {1}
|
|
# pins are 80000 x 80000 DSN units apart -> Manhattan = 160000, scaled x10
|
|
assert _length(ortho) == pytest.approx(1_600_000.0)
|
|
|
|
|
|
def test_diagonal_is_strictly_shorter_and_ideal():
|
|
"""Diagonal on connects the same net with a single 45-degree trace at the ideal
|
|
hypotenuse length -- strictly shorter than the orthogonal staircase."""
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
ortho, _, _ = route_dsn_board_exact(dsn)
|
|
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
assert diag.routed_net_numbers == {1}
|
|
ideal = math.hypot(800_000, 800_000) # board units
|
|
assert _length(diag) == pytest.approx(ideal)
|
|
assert _length(diag) < _length(ortho)
|
|
# a real, sizeable win, not a rounding artifact
|
|
assert _length(diag) / _length(ortho) < 0.72
|
|
|
|
|
|
def test_diagonal_route_is_a_single_45_degree_segment():
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
segs = diag.result.wires[1]
|
|
assert len(segs) == 1
|
|
_layer, pts = segs[0]
|
|
assert len(pts) == 2
|
|
(ax, ay), (bx, by) = (pts[0].x, pts[0].y), (pts[1].x, pts[1].y)
|
|
assert abs(bx - ax) == abs(by - ay) != 0 # exactly 45 degrees
|
|
|
|
|
|
def test_diagonal_endpoints_stay_on_pads():
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
pins = _pins_by_net(DIAGONAL_WIN)
|
|
for net_no, segments in diag.result.wires.items():
|
|
pts = [(p.x, p.y) for _layer, run in segments for p in run]
|
|
assert {pts[0], pts[-1]} == pins[net_no]
|
|
|
|
|
|
# --- DRC-clean, verified exactly and independently ---------------------------
|
|
|
|
|
|
def test_diagonal_is_drc_clean_via_tree():
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
clearance = _rule_clearance_board(DIAGONAL_WIN)
|
|
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
assert diag.drc_clean
|
|
assert diag.tree.has_violation(clearance) is None
|
|
|
|
|
|
def test_diagonal_is_drc_clean_independently_of_the_tree():
|
|
"""Reconstruct the 45-degree copper's clearance octagon from the emitted route
|
|
and confirm it clears every different-net static copper (pads, the keepout) --
|
|
a check that never consults the search tree the router verified against."""
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
board = build_board(dsn)
|
|
clearance = _rule_clearance_board(DIAGONAL_WIN)
|
|
half_width = 10_000 # trace_width 2000 * scale 10 / 2
|
|
|
|
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
|
|
# different-net static copper: the keepout (net -1); pads share NET_A here
|
|
obstacles = []
|
|
for obstacle in board.get_obstacle_areas():
|
|
for tile in obstacle.tiles:
|
|
obstacles.append(tile.bounding_box())
|
|
|
|
assert diag.result.wires # sanity: the diagonal net was actually emitted
|
|
conflicts = 0
|
|
for _net_no, segments in diag.result.wires.items():
|
|
for _layer, pts in segments:
|
|
for p, q in zip(pts, pts[1:], strict=False):
|
|
grown = segment_octagon(p, q, half_width + clearance)
|
|
for box in obstacles:
|
|
if overlaps_2d(grown, box):
|
|
conflicts += 1
|
|
assert conflicts == 0
|
|
|
|
|
|
def test_exact_clearance_is_load_bearing():
|
|
"""The keepout sits INSIDE the diagonal's bounding box (so the coarse bbox
|
|
clearance the plain router uses would reject the diagonal) yet clears the exact
|
|
octagon -- proving the octagon, not the box, is what lets the trace route."""
|
|
a, b = IntPoint(200_000, -200_000), IntPoint(1_000_000, -1_000_000)
|
|
half_width, clearance = 10_000, 20_000
|
|
bbox = IntBox(a.x - half_width, b.y - half_width, b.x + half_width, a.y + half_width)
|
|
keepout = IntBox(220_000, -980_000, 400_000, -800_000)
|
|
assert not keepout.intersection(bbox).is_empty() # inside the coarse cover
|
|
assert not overlaps_2d(segment_octagon(a, b, half_width + clearance), keepout) # clear exactly
|
|
|
|
|
|
def test_diagonal_is_deterministic():
|
|
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
|
a, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
b, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
assert _wire_key(a) == _wire_key(b)
|
|
assert a.routed_net_numbers == b.routed_net_numbers
|
|
|
|
|
|
def test_diagonal_valid_ses():
|
|
ses = route(DIAGONAL_WIN.read_text(), engine="exact", diagonal=True)
|
|
top = parse(ses)
|
|
assert top.head == "session"
|
|
nets = top.child("routes").child("network_out").children("net")
|
|
assert {n.values()[0].text for n in nets} == {"NET_A"}
|
|
|
|
|
|
# --- additive: diagonal off changes nothing on the orthogonal track ----------
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", [DIAGONAL_WIN, SIMPLE, CROSSING, KICAD])
|
|
def test_diagonal_off_matches_orthogonal(fixture):
|
|
dsn = parse_dsn(fixture.read_text())
|
|
base, _, _ = route_dsn_board_exact(dsn)
|
|
off, _, _ = route_dsn_board_exact(dsn, diagonal=False)
|
|
assert _wire_key(base) == _wire_key(off)
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, KICAD])
|
|
def test_diagonal_on_keeps_existing_fixtures_clean_and_connected(fixture):
|
|
"""Turning diagonal on never dirties the DRC or drops a net that orthogonal
|
|
routed -- it only ever shortens."""
|
|
dsn = parse_dsn(fixture.read_text())
|
|
clearance = _rule_clearance_board(fixture)
|
|
base, _, _ = route_dsn_board_exact(dsn)
|
|
on, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
|
assert on.drc_clean
|
|
assert on.tree.has_violation(clearance) is None
|
|
assert on.routed_net_numbers >= base.routed_net_numbers
|
|
assert _length(on) <= _length(base) + 1e-6
|