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.
155 lines
6.0 KiB
Python
155 lines
6.0 KiB
Python
"""Tests for the exact-geometry (orthogonal, DRC-verified) routing track.
|
|
|
|
The headline invariant: the exact router's output has **no clearance violation**
|
|
by exact ``IntBox`` intersection — where the grid MVP's centrelines, given real
|
|
width, may. Endpoints stay exactly on pads; vias sit at real layer transitions;
|
|
connectivity parity holds against the JAR oracle on the designed boards.
|
|
|
|
Fixtures: simple_2net (single-layer), crossing_2net (needs a via), ripup_needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from rpmroute.board import ShapeSearchTree, TreeShape, build_board
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.dsn.sexp import parse
|
|
from rpmroute.geometry import IntBox, Polyline, PolylineShape
|
|
from rpmroute.route import route, route_dsn_board, route_dsn_board_exact
|
|
from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
|
SIMPLE = FIXTURES / "simple_2net.dsn"
|
|
CROSSING = FIXTURES / "crossing_2net.dsn"
|
|
RIPUP = FIXTURES / "ripup_needed.dsn"
|
|
KICAD = FIXTURES / "kicad_routable.dsn"
|
|
|
|
|
|
def _tree_from_result(board, result, half_width, signal_layers):
|
|
"""Index a RouteResult's copper (with width) plus pads into a search tree."""
|
|
tree = ShapeSearchTree()
|
|
owner = 0
|
|
for pin in board.get_pins():
|
|
if pin.shape is None:
|
|
continue
|
|
net = pin.net_nos[0] if pin.net_nos else -1
|
|
layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
|
if layers:
|
|
tree.insert(TreeShape(owner, net, layers, pin.shape.bounding_box()))
|
|
owner += 1
|
|
for net_no, segments in result.wires.items():
|
|
for layer, points in segments:
|
|
for box in PolylineShape(Polyline(points), half_width).tiles():
|
|
tree.insert(TreeShape(owner, net_no, frozenset({layer}), box, routed=True))
|
|
owner += 1
|
|
for net_no, locations in result.vias.items():
|
|
for loc in locations:
|
|
box = IntBox(
|
|
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
|
)
|
|
tree.insert(TreeShape(owner, net_no, frozenset(signal_layers), box, routed=True))
|
|
owner += 1
|
|
return tree
|
|
|
|
|
|
# --- headline: exact router output is DRC-clean -----------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, RIPUP, KICAD])
|
|
def test_exact_router_output_is_drc_clean(fixture):
|
|
dsn = parse_dsn(fixture.read_text())
|
|
exact, scale, _ = route_dsn_board_exact(dsn)
|
|
assert exact.drc_clean
|
|
clearance = round(_rule_clearance_dsn(dsn) * scale)
|
|
assert exact.tree.has_violation(clearance) is None
|
|
|
|
|
|
def test_exact_router_connects_all_nets_on_designed_boards():
|
|
for fixture in (SIMPLE, CROSSING, RIPUP):
|
|
exact, _, _ = route_dsn_board_exact(parse_dsn(fixture.read_text()))
|
|
assert exact.routed_net_numbers == {1, 2}
|
|
|
|
|
|
def test_exact_crossing_uses_a_via():
|
|
exact, _, _ = route_dsn_board_exact(parse_dsn(CROSSING.read_text()))
|
|
assert exact.via_count() >= 1
|
|
|
|
|
|
# --- exact geometry properties ----------------------------------------------
|
|
|
|
|
|
def test_exact_endpoints_are_exactly_on_pads():
|
|
exact, _, _ = route_dsn_board_exact(parse_dsn(SIMPLE.read_text()))
|
|
board = build_board(parse_dsn(SIMPLE.read_text()))
|
|
pins_by_net: dict[int, set[tuple[int, int]]] = {}
|
|
for pin in board.get_pins():
|
|
for net_no in pin.net_nos:
|
|
pins_by_net.setdefault(net_no, set()).add((pin.location.x, pin.location.y))
|
|
for net_no, segments in exact.result.wires.items():
|
|
pts = [(p.x, p.y) for _, seg in segments for p in seg]
|
|
assert {pts[0], pts[-1]} == pins_by_net[net_no]
|
|
|
|
|
|
def test_exact_traces_are_orthogonal():
|
|
exact, _, _ = route_dsn_board_exact(parse_dsn(CROSSING.read_text()))
|
|
for segments in exact.result.wires.values():
|
|
for _, points in segments:
|
|
for a, b in Polyline(points).segments():
|
|
assert a.x == b.x or a.y == b.y # axis-aligned
|
|
|
|
|
|
def test_exact_engine_emits_valid_ses_with_vias():
|
|
ses = route(CROSSING.read_text(), engine="exact")
|
|
assert parse(ses).head == "session"
|
|
network = parse(ses).child("routes").child("network_out")
|
|
total_vias = sum(len(net.children("via")) for net in network.children("net"))
|
|
assert total_vias >= 1
|
|
|
|
|
|
# --- exact passes DRC where the grid MVP does not ---------------------------
|
|
|
|
|
|
def test_exact_clean_where_grid_mvp_violates_on_real_board():
|
|
dsn = parse_dsn(KICAD.read_text())
|
|
board = build_board(dsn)
|
|
scale = dsn.resolution.value
|
|
half_width = round(_rule_width_dsn(dsn) * scale) // 2
|
|
clearance = round(_rule_clearance_dsn(dsn) * scale)
|
|
signal_layers = [i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal]
|
|
|
|
# grid MVP (8-connected): route, then give the centrelines real width
|
|
grid_result, _, _ = route_dsn_board(dsn)
|
|
grid_tree = _tree_from_result(board, grid_result, half_width, signal_layers)
|
|
grid_has_violation = grid_tree.has_violation(clearance) is not None
|
|
|
|
# exact track: DRC-clean by construction
|
|
exact, _, _ = route_dsn_board_exact(dsn)
|
|
|
|
assert exact.drc_clean
|
|
# the grid MVP's widened centrelines are not DRC-clean on this dense board;
|
|
# the exact track is. (If a future grid change makes it clean too, that is
|
|
# still fine — the exact track's guarantee is the point.)
|
|
assert grid_has_violation
|
|
|
|
|
|
# --- oracle parity ----------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.oracle
|
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING])
|
|
def test_exact_connectivity_parity_with_oracle(fixture):
|
|
from oracle import HAS_ORACLE, route_dsn, routed_net_set
|
|
|
|
if not HAS_ORACLE:
|
|
pytest.skip("FreeRouting oracle unavailable")
|
|
ours = routed_net_set(route(fixture.read_text(), engine="exact"))
|
|
theirs = routed_net_set(route_dsn(fixture, max_passes=3, timeout=300))
|
|
assert theirs, "oracle routed nothing on a routable board"
|
|
assert theirs <= ours
|