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.
122 lines
4.3 KiB
Python
122 lines
4.3 KiB
Python
"""Tests for the continuous expansion-room routing track.
|
|
|
|
The room router decomposes free space into exact rectangles (rooms) joined by
|
|
doors, instead of a fixed grid. The headline result is the *continuity win*: a
|
|
board with a narrow, off-grid channel that the grid/exact track cannot route
|
|
(no cell centre lands in the channel) but the room track can.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from rpmroute.board import build_board
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.dsn.sexp import parse
|
|
from rpmroute.geometry import Polyline
|
|
from rpmroute.route import route, route_dsn_board_exact, route_dsn_board_rooms
|
|
|
|
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"
|
|
KICAD = FIXTURES / "kicad_routable.dsn"
|
|
NARROW = FIXTURES / "narrow_channel.dsn"
|
|
SHOVE_CHANNEL = FIXTURES / "rooms_shove_channel.dsn"
|
|
WIDE_DOOR = FIXTURES / "rooms_wide_door.dsn"
|
|
|
|
|
|
def _clearance(dsn, scale):
|
|
from rpmroute.route.pipeline import _rule_clearance_dsn
|
|
|
|
return round(_rule_clearance_dsn(dsn) * scale)
|
|
|
|
|
|
# --- DRC-clean invariant (same non-negotiable as the exact track) -----------
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, KICAD, NARROW, SHOVE_CHANNEL, WIDE_DOOR])
|
|
def test_room_router_output_is_drc_clean(fixture):
|
|
dsn = parse_dsn(fixture.read_text())
|
|
rooms, scale, _ = route_dsn_board_rooms(dsn)
|
|
assert rooms.drc_clean
|
|
assert rooms.tree.has_violation(_clearance(dsn, scale)) is None
|
|
|
|
|
|
# --- connectivity + geometry ------------------------------------------------
|
|
|
|
|
|
def test_room_router_connects_simple_and_crossing():
|
|
for fixture in (SIMPLE, CROSSING):
|
|
rooms, _, _ = route_dsn_board_rooms(parse_dsn(fixture.read_text()))
|
|
assert rooms.routed_net_numbers == {1, 2}
|
|
|
|
|
|
def test_room_router_uses_a_via_on_the_crossing_board():
|
|
rooms, _, _ = route_dsn_board_rooms(parse_dsn(CROSSING.read_text()))
|
|
assert rooms.via_count() >= 1
|
|
|
|
|
|
def test_room_router_routes_the_real_boards_multipin_nets():
|
|
dsn = parse_dsn(KICAD.read_text())
|
|
rooms, _, _ = route_dsn_board_rooms(dsn)
|
|
multi = sum(1 for n in dsn.nets if len(n.pins) >= 2)
|
|
assert len(rooms.routed_net_numbers) == multi == 4
|
|
|
|
|
|
def test_room_traces_are_orthogonal_and_end_on_pads():
|
|
rooms, _, _ = route_dsn_board_rooms(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 rooms.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]
|
|
for _, points in segments:
|
|
for a, b in Polyline(points).segments():
|
|
assert a.x == b.x or a.y == b.y
|
|
|
|
|
|
def test_room_engine_emits_valid_ses():
|
|
ses = route(SIMPLE.read_text(), engine="room")
|
|
assert parse(ses).head == "session"
|
|
|
|
|
|
# --- the headline: continuity beats the grid --------------------------------
|
|
|
|
|
|
def test_room_router_routes_a_channel_the_grid_cannot():
|
|
dsn = parse_dsn(NARROW.read_text())
|
|
# the exact/grid track cannot fit a centreline in the narrow, off-grid gap
|
|
exact, scale, _ = route_dsn_board_exact(dsn)
|
|
assert exact.routed_net_numbers == set() # grid quantization drops the net
|
|
assert exact.drc_clean
|
|
|
|
# the continuous room track routes it — and stays DRC-clean
|
|
rooms, _, _ = route_dsn_board_rooms(dsn)
|
|
assert rooms.routed_net_numbers == {1}
|
|
assert rooms.drc_clean
|
|
assert rooms.tree.has_violation(_clearance(dsn, scale)) is None
|
|
|
|
|
|
# --- oracle parity ----------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.oracle
|
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING])
|
|
def test_room_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="room"))
|
|
theirs = routed_net_set(route_dsn(fixture, max_passes=3, timeout=300))
|
|
assert theirs, "oracle routed nothing on a routable board"
|
|
assert theirs <= ours
|