Adds route/exact_router.py: routes with the grid track's rip-up/multi-layer/ via topology search in orthogonal mode (axis-aligned segments -> exact IntBox copper), then verifies clearance exactly against a ShapeSearchTree. A net whose copper would come closer than the clearance to any accepted item is dropped, so the emitted geometry is DRC-clean by construction; endpoints stay exactly on pads and vias at real layer transitions. GridRouter gains a non-breaking orthogonal option (4-connected). The pipeline gains an engine= selector: 'grid' (default, unchanged, highest coverage) or 'exact' (DRC-verified). The grid MVP stays the fallback. On the designed boards the exact track routes every net DRC-clean; on the real KiCad board it matches the grid's connectivity (4/4 multi-pin nets) and is DRC-clean where the grid MVP's widened centrelines are not. Deferred: shove, and 45-degree/IntOctagon trace caps. Tests: the exact router's output has no clearance violation (exact IntBox check) on every fixture; endpoints on pads; traces orthogonal; crossing uses a via; and a direct comparison showing the exact track is clean where the grid MVP violates on the real board. Oracle-gated parity for engine='exact'.
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 freeroute.board import ShapeSearchTree, TreeShape, build_board
|
|
from freeroute.dsn import parse_dsn
|
|
from freeroute.dsn.sexp import parse
|
|
from freeroute.geometry import IntBox, Polyline, PolylineShape
|
|
from freeroute.route import route, route_dsn_board, route_dsn_board_exact
|
|
from freeroute.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
|