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.
118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""Tests for shove-and-retry on the exact-geometry track.
|
|
|
|
Shove moves an existing trace aside to make room for a net that would otherwise
|
|
be dropped, without ever producing a clearance violation.
|
|
|
|
The dense fixture ``shove_needed.dsn`` has one signal layer (no vias): with
|
|
greedy net ordering NET_A routes as a straight wall that boxes NET_C out, so
|
|
without shove NET_C is dropped; with shove NET_A is moved aside and both
|
|
connect, DRC-clean. (Rip-up would also reroute NET_A here; the shove test uses
|
|
greedy ordering to isolate the shove mechanism.)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.dsn.sexp import parse
|
|
from rpmroute.route import route, route_dsn_board_exact
|
|
from rpmroute.route.pipeline import _rule_clearance_dsn
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
|
SHOVE = FIXTURES / "shove_needed.dsn"
|
|
REGRESSION = ["simple_2net", "crossing_2net", "ripup_needed", "kicad_routable"]
|
|
|
|
|
|
def _exact(fixture, shove):
|
|
return route_dsn_board_exact(parse_dsn(fixture.read_text()), shove=shove)
|
|
|
|
|
|
# --- the headline shove contrast (greedy ordering isolates the mechanism) ----
|
|
|
|
|
|
def test_without_shove_a_net_is_dropped():
|
|
# greedy exact (no shove) cannot fit NET_C past NET_A's wall
|
|
off, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=False)
|
|
assert len(off.routed_net_numbers) < 2
|
|
assert off.drc_clean # still clean, just incomplete
|
|
|
|
|
|
def test_shove_connects_all_and_stays_drc_clean():
|
|
on, scale, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
|
assert on.routed_net_numbers == {1, 2} # both nets connected
|
|
assert on.shoves >= 1 # at least one trace was moved aside
|
|
assert on.drc_clean
|
|
clearance = round(_rule_clearance_dsn(parse_dsn(SHOVE.read_text())) * scale)
|
|
assert on.tree.has_violation(clearance) is None # exact DRC check
|
|
|
|
|
|
def test_shove_result_is_deterministic():
|
|
a, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
|
b, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
|
|
|
def key(r):
|
|
return {
|
|
n: [(layer, [(p.x, p.y) for p in pts]) for layer, pts in segs]
|
|
for n, segs in r.result.wires.items()
|
|
}
|
|
|
|
assert key(a) == key(b)
|
|
|
|
|
|
def test_shoved_trace_endpoints_still_on_pads():
|
|
from rpmroute.board import build_board
|
|
|
|
on, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
|
board = build_board(parse_dsn(SHOVE.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 on.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_shove_emits_valid_ses():
|
|
ses = route(SHOVE.read_text(), engine="exact", shove=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", "NET_C"}
|
|
|
|
|
|
# --- regression: shove is a no-op where it isn't needed ----------------------
|
|
|
|
|
|
@pytest.mark.parametrize("name", REGRESSION)
|
|
def test_shove_does_not_change_or_break_normal_boards(name):
|
|
fixture = FIXTURES / f"{name}.dsn"
|
|
off, _, _ = _exact(fixture, shove=False)
|
|
on, _, _ = _exact(fixture, shove=True)
|
|
# same connectivity, both DRC-clean, and no shove was needed
|
|
assert on.routed_net_numbers == off.routed_net_numbers
|
|
assert off.drc_clean and on.drc_clean
|
|
assert on.shoves == 0
|
|
|
|
|
|
# --- oracle parity ----------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.oracle
|
|
def test_shove_connectivity_parity_with_oracle():
|
|
from oracle import HAS_ORACLE, route_dsn, routed_net_set
|
|
|
|
if not HAS_ORACLE:
|
|
pytest.skip("FreeRouting oracle unavailable")
|
|
ours = routed_net_set(route(SHOVE.read_text(), engine="exact", shove=True))
|
|
theirs = routed_net_set(route_dsn(SHOVE, max_passes=3, timeout=300))
|
|
if not theirs:
|
|
pytest.skip("reference JAR routed nothing on the synthetic single-layer board")
|
|
assert theirs <= ours
|