Files
rpmroute/tests/route/test_room_pack.py
T
rsp2k a6aa1991a1 Rename to rpmroute
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.
2026-10-07 14:10:36 -06:00

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