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.
234 lines
9.1 KiB
Python
234 lines
9.1 KiB
Python
"""Tests for coordinated multi-trace channel packing on the room router.
|
|
|
|
``pack=True`` is a third opt-in room-track upgrade, independent of ``shove``. The
|
|
skeptic's finding that motivated it: shove recovers ordering drops but does not
|
|
create density; the real densifier is lane packing. When several 2-pin nets must
|
|
cross a common obstacle gap, greedy per-net routing lets each net grab a gate
|
|
independently, the paths collide, and some nets drop even though the gap is
|
|
physically wide enough for all of them (``channel_pack.dsn`` -- plain routing,
|
|
shove on or off, drops a net under *every* net ordering). The packer assigns each
|
|
net a parallel lane across the gap's usable width (pad-ordered, spaced >= width +
|
|
clearance) with staggered fan-in trunks, routes them together, and connects all
|
|
of them DRC-clean.
|
|
|
|
The win is bounded honestly by geometry: the packer succeeds iff the gap is at
|
|
least ``(N-1)*(width+clearance) + 2*(half_width+clearance)`` wide -- the true
|
|
feasibility width for N lanes -- and never regresses below greedy below it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import itertools
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.dsn.sexp import parse
|
|
from rpmroute.geometry import Polyline, PolylineShape
|
|
from rpmroute.route import route, route_dsn_board_rooms
|
|
from rpmroute.route.pipeline import _rule_clearance_dsn
|
|
|
|
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
|
CHANNEL_PACK = FIXTURES / "channel_pack.dsn"
|
|
SIMPLE = FIXTURES / "simple_2net.dsn"
|
|
CROSSING = FIXTURES / "crossing_2net.dsn"
|
|
|
|
|
|
def _clearance(dsn, scale):
|
|
return round(_rule_clearance_dsn(dsn) * scale)
|
|
|
|
|
|
def _emitted_drc_violations(result, clearance):
|
|
"""Independent DRC over the emitted route: reconstruct every wire's copper
|
|
tiles and return different-net, same-layer pairs closer than ``clearance``.
|
|
Does not consult the search tree, so it catches copper emitted but (via a bug)
|
|
absent from the tree."""
|
|
items = []
|
|
for net_no, segments in result.wires.items():
|
|
for layer, corners in segments:
|
|
for box in PolylineShape(Polyline(corners), result.half_width).tiles():
|
|
items.append((net_no, layer, box))
|
|
bad = []
|
|
for i in range(len(items)):
|
|
n1, l1, b1 = items[i]
|
|
expanded = b1.offset(clearance)
|
|
for j in range(i + 1, len(items)):
|
|
n2, l2, b2 = items[j]
|
|
if n1 == n2 or l1 != l2:
|
|
continue
|
|
if b2.overlaps(expanded):
|
|
bad.append((n1, n2))
|
|
return bad
|
|
|
|
|
|
def _net_blocks(text):
|
|
import re
|
|
|
|
return re.findall(r" \(net [^\n]*\n \(pins [^\n]*\n \)\n", text)
|
|
|
|
|
|
def _reorder(text, perm):
|
|
blocks = _net_blocks(text)
|
|
return text.replace("".join(blocks), "".join(blocks[i] for i in perm), 1)
|
|
|
|
|
|
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 _routed_counts(text, n, *, pack, shove):
|
|
return [
|
|
len(
|
|
route_dsn_board_rooms(parse_dsn(_reorder(text, p)), shove=shove, pack=pack)[
|
|
0
|
|
].routed_net_numbers
|
|
)
|
|
for p in itertools.permutations(range(n))
|
|
]
|
|
|
|
|
|
# --- the genuine gap: current router drops a net under every ordering ---------
|
|
|
|
|
|
def test_channel_pack_plain_drops_a_net_under_every_ordering():
|
|
"""Bar (same methodology as true_density): the current router, shove on OR
|
|
off, drops >= 1 of the three nets under EVERY net ordering -- an
|
|
order-independent drop, even though the gap physically fits three lanes."""
|
|
text = CHANNEL_PACK.read_text()
|
|
plain = _routed_counts(text, 3, pack=False, shove=False)
|
|
shoved = _routed_counts(text, 3, pack=False, shove=True)
|
|
assert all(k < 3 for k in plain)
|
|
assert all(k < 3 for k in shoved)
|
|
# shove buys nothing here: it relocates blockers, it does not pack lanes
|
|
assert shoved == plain
|
|
|
|
|
|
def test_channel_pack_connects_all_drc_clean():
|
|
"""The packer connects all three nets and the emitted copper is exactly
|
|
DRC-clean, verified both through the search tree and independently."""
|
|
dsn = parse_dsn(CHANNEL_PACK.read_text())
|
|
on, scale, _ = route_dsn_board_rooms(dsn, pack=True)
|
|
clearance = _clearance(dsn, scale)
|
|
assert on.routed_net_numbers == {1, 2, 3}
|
|
assert on.drc_clean
|
|
assert on.tree.has_violation(clearance) is None
|
|
assert _emitted_drc_violations(on.result, clearance) == []
|
|
|
|
|
|
def test_channel_pack_connects_all_under_every_ordering():
|
|
counts = _routed_counts(CHANNEL_PACK.read_text(), 3, pack=True, shove=False)
|
|
assert counts == [3, 3, 3, 3, 3, 3]
|
|
|
|
|
|
def test_channel_pack_is_deterministic():
|
|
a, _, _ = route_dsn_board_rooms(parse_dsn(CHANNEL_PACK.read_text()), pack=True)
|
|
b, _, _ = route_dsn_board_rooms(parse_dsn(CHANNEL_PACK.read_text()), pack=True)
|
|
assert _wire_key(a) == _wire_key(b)
|
|
assert a.routed_net_numbers == b.routed_net_numbers
|
|
|
|
|
|
def test_channel_pack_endpoints_stay_on_pads():
|
|
dsn = parse_dsn(CHANNEL_PACK.read_text())
|
|
board_pins: dict[int, set[tuple[int, int]]] = {}
|
|
from rpmroute.board import build_board
|
|
|
|
for pin in build_board(dsn).get_pins():
|
|
for net_no in pin.net_nos:
|
|
board_pins.setdefault(net_no, set()).add((pin.location.x, pin.location.y))
|
|
on, _, _ = route_dsn_board_rooms(dsn, pack=True)
|
|
for net_no, segments in on.result.wires.items():
|
|
pts = [(p.x, p.y) for _, run in segments for p in run]
|
|
assert {pts[0], pts[-1]} == board_pins[net_no]
|
|
|
|
|
|
def test_channel_pack_valid_ses():
|
|
ses = route(CHANNEL_PACK.read_text(), engine="room", pack=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} == {"NET0", "NET1", "NET2"}
|
|
|
|
|
|
# --- additive: pack must not disturb the room-default / non-channel tracks ----
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, CHANNEL_PACK])
|
|
def test_pack_off_matches_default(fixture):
|
|
"""pack defaults off; passing pack=False must be byte-identical to the
|
|
foundation room route (no channel behaviour leaks into the default track)."""
|
|
dsn = parse_dsn(fixture.read_text())
|
|
default, _, _ = route_dsn_board_rooms(dsn)
|
|
off, _, _ = route_dsn_board_rooms(dsn, pack=False)
|
|
assert _wire_key(default) == _wire_key(off)
|
|
|
|
|
|
def test_pack_on_non_channel_board_is_noop():
|
|
"""On a board with no obstacle gap to pack, pack=True changes nothing: the
|
|
pre-pass finds no channel group and the greedy loop runs exactly as before."""
|
|
dsn = parse_dsn(SIMPLE.read_text())
|
|
off, _, _ = route_dsn_board_rooms(dsn, pack=False)
|
|
on, _, _ = route_dsn_board_rooms(dsn, pack=True)
|
|
assert _wire_key(off) == _wire_key(on)
|
|
|
|
|
|
# --- falsifiable: pack succeeds iff the gap meets the geometric feasibility ----
|
|
|
|
|
|
def _channel_dsn(n, gap, spread, *, width=200000, height=80000):
|
|
gx0 = (width - gap) // 2
|
|
gx1 = (width + gap) // 2
|
|
cx = width // 2
|
|
xs = [int(round(cx + (i - (n - 1) / 2) * spread)) for i in range(n)]
|
|
places = "".join(
|
|
f" (place P{i}a {x} -10000 front 0)\n (place P{i}b {x} -70000 front 0)\n"
|
|
for i, x in enumerate(xs)
|
|
)
|
|
nets = "".join(f" (net NET{i}\n (pins P{i}a-1 P{i}b-1)\n )\n" for i in range(n))
|
|
return f"""(pcb "sweep.dsn"
|
|
(parser (string_quote ") (space_in_quoted_tokens on) (host_cad "t") (host_version "1"))
|
|
(resolution um 10)
|
|
(unit um)
|
|
(structure
|
|
(layer F.Cu (type signal) (property (index 0)))
|
|
(boundary (path pcb 0 0 0 {width} 0 {width} -{height} 0 -{height} 0 0))
|
|
(keepout "wl" (rect F.Cu 0 -42000 {gx0} -38000))
|
|
(keepout "wr" (rect F.Cu {gx1} -42000 {width} -38000))
|
|
(rule (width 2000) (clearance 2000)))
|
|
(library
|
|
(padstack Rect_Pad (shape (rect F.Cu -1000 -1000 1000 1000)) (attach off))
|
|
(image PAD (pin Rect_Pad 1 0 0)))
|
|
(placement (component PAD
|
|
{places} ))
|
|
(network
|
|
{nets} (class default (rule (width 2000) (clearance 2000))))
|
|
(wiring )
|
|
)
|
|
"""
|
|
|
|
|
|
@pytest.mark.parametrize("n", [3, 4])
|
|
def test_channel_pack_boundary_is_geometric_feasibility(n):
|
|
"""Sweep the gap width: the packer connects all N nets iff the gap is at least
|
|
the geometric feasibility width for N lanes, and never routes fewer than the
|
|
greedy baseline below it. width=2000, clearance=2000 -> half_width=1000, so
|
|
lanes need >= 4000 apart and >= 3000 from each gap edge."""
|
|
width, clearance, half_width = 2000, 2000, 1000
|
|
feasibility = (n - 1) * (width + clearance) + 2 * (half_width + clearance)
|
|
spread = 40000
|
|
step = 1000
|
|
for gap in range(feasibility - 4 * step, feasibility + 4 * step + 1, step):
|
|
text = _channel_dsn(n, gap, spread)
|
|
plain = len(route_dsn_board_rooms(parse_dsn(text), pack=False)[0].routed_net_numbers)
|
|
packed_res = route_dsn_board_rooms(parse_dsn(text), pack=True)[0]
|
|
packed = len(packed_res.routed_net_numbers)
|
|
assert packed_res.drc_clean
|
|
assert packed >= plain # never a regression
|
|
if gap >= feasibility:
|
|
assert packed == n # packs everything at/above feasibility
|
|
else:
|
|
assert packed < n # cannot (and does not falsely) pack below it
|