Files
rpmroute/tests/route/test_room_shove.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

456 lines
20 KiB
Python

"""Tests for gate-optimal placement and shove-in-rooms on the room router.
Two opt-in (``shove=True``) upgrades to the continuous expansion-room track:
* **Occupancy-aware gate placement** seats a trace at the best point in a door
rather than the raw midpoint. With ``margin = clearance + half_width`` the room
decomposition already keeps every door clearance-clear of committed copper, so
the projection provably reduces to the old clamp where nothing else crosses the
door — the four foundation fixtures stay byte-for-byte identical.
* **Shove-in-rooms** nudges a committed trace aside with the exact shove
primitive when a net would otherwise be dropped, so *both* fit through the same
channel. ``rooms_shove_channel`` drops a net under the default greedy net order
and routes both with shove, DRC-clean -- a recovery from greedy ordering (the
same board is plainly routable under a reorder or without its cosmetic seals,
see ``test_room_shove_channel_drop_is_ordering_artifact_not_density``), not a
genuine density limit. The true sub-cell win is occupancy packing, which the
``rooms_wide_door`` fixture exercises with ``shoves == 0``.
The ``true_density`` fixture settles the density question the other way and honestly:
plain routing drops a net under EVERY net ordering (an order-independent obstruction,
not a self-inflicted greedy choice), and shove recovers *nothing* there (``shoves == 0``).
shove relocates a blocker into existing free space; it does not compress copper, so it
cannot manufacture density where none exists. The recover/fail boundary is exactly the
free-space boundary -- ``test_true_density_contrast_is_only_the_wall_height`` shows the
two boards differ only in how far the wall spans.
"""
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 IntBox, IntPoint, Polyline, PolylineShape
from rpmroute.route import route, route_dsn_board_exact, route_dsn_board_rooms
from rpmroute.route.pipeline import _rule_clearance_dsn
def _emitted_drc_violations(result, clearance):
"""Independent DRC over the *emitted* route: reconstruct every wire's copper
tiles and return the different-net, same-layer pairs that come closer than
``clearance``. This does NOT consult the search tree, so it catches copper
that is present in the output but (via a bug) absent from the tree -- exactly
the failure mode the wire/owner desync produced."""
items = [] # (net_no, layer, tile)
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
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"
MULTIPIN_DESYNC = FIXTURES / "rooms_multipin_desync.dsn"
MULTIPIN_DESYNC_CRASH = FIXTURES / "rooms_multipin_desync_crash.dsn"
TRUE_DENSITY = FIXTURES / "true_density.dsn"
FOUNDATION = [SIMPLE, CROSSING, KICAD, NARROW]
def _clearance(dsn, scale):
return round(_rule_clearance_dsn(dsn) * scale)
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.wires.items()
}
def _pins_by_net(fixture):
board = build_board(parse_dsn(fixture.read_text()))
out: dict[int, set[tuple[int, int]]] = {}
for pin in board.get_pins():
for net_no in pin.net_nos:
out.setdefault(net_no, set()).add((pin.location.x, pin.location.y))
return out
# --- shove recovers a greedy-ordering drop ----------------------------------
def test_room_shove_channel_recovers_greedy_ordering_drop():
dsn = parse_dsn(SHOVE_CHANNEL.read_text())
off, _, _ = route_dsn_board_rooms(dsn, shove=False)
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
# under the default greedy net order NET_A (net 1) walls the channel -> NET_C drops
assert off.routed_net_numbers == {1}
assert off.drc_clean
# with shove NET_A is nudged aside and both nets fit through the same channel
assert on.routed_net_numbers == {1, 2}
assert on.shoves >= 1
assert on.drc_clean
assert on.tree.has_violation(_clearance(dsn, scale)) is None
def test_room_shove_channel_drop_is_ordering_artifact_not_density():
"""Honest bound on the shove_channel claim: the plain drop is an artifact of
the greedy net order, not a genuine density limit. The identical plain engine
routes BOTH nets with zero shoves if NET_C is listed first, or if the two
cosmetic edge seals are removed -- so shove here recovers a self-inflicted
ordering drop rather than overcoming an unroutable density. (The genuine
sub-cell win is occupancy packing, exercised by the wide_door fixture with
shoves == 0.)"""
orig = SHOVE_CHANNEL.read_text()
# (1) reorder so NET_C becomes net 1 -> plain routing solves it, no shove
swapped = orig.replace(
" (net NET_A\n (pins A1-1 A2-1)\n )\n"
" (net NET_C\n (pins C1-1 C2-1)\n )",
" (net NET_C\n (pins C1-1 C2-1)\n )\n"
" (net NET_A\n (pins A1-1 A2-1)\n )",
)
assert swapped != orig
reordered, _, _ = route_dsn_board_rooms(parse_dsn(swapped), shove=False)
assert reordered.routed_net_numbers == {1, 2}
assert reordered.shoves == 0
assert reordered.drc_clean
# (2) drop the seal keepouts -> plain routing solves it, no shove
noseal = orig.replace(
' (keepout "seal_left"\n (rect F.Cu 0 -30000 2500 0)\n )\n'
' (keepout "seal_right"\n (rect F.Cu 197500 -30000 200000 0)\n )\n',
"",
)
assert noseal != orig
unsealed, _, _ = route_dsn_board_rooms(parse_dsn(noseal), shove=False)
assert unsealed.routed_net_numbers == {1, 2}
assert unsealed.shoves == 0
assert unsealed.drc_clean
def test_room_shove_channel_moves_blocker_but_keeps_its_pads():
on, _, _ = route_dsn_board_rooms(parse_dsn(SHOVE_CHANNEL.read_text()), shove=True)
pins = _pins_by_net(SHOVE_CHANNEL)
# NET_A (net 1) is the shoved blocker: its endpoints stay on its pads...
seg = on.result.wires[1]
pts = [(p.x, p.y) for _, run in seg for p in run]
assert {pts[0], pts[-1]} == pins[1]
# ...but a middle corner moved off the original straight y (the shove jog)
ys = {y for _, y in pts}
assert len(ys) > 1
def test_room_shove_channel_endpoints_on_pads():
on, _, _ = route_dsn_board_rooms(parse_dsn(SHOVE_CHANNEL.read_text()), shove=True)
pins = _pins_by_net(SHOVE_CHANNEL)
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]} == pins[net_no]
def test_room_shove_channel_is_deterministic():
a, _, _ = route_dsn_board_rooms(parse_dsn(SHOVE_CHANNEL.read_text()), shove=True)
b, _, _ = route_dsn_board_rooms(parse_dsn(SHOVE_CHANNEL.read_text()), shove=True)
assert _wire_key(a.result) == _wire_key(b.result)
assert a.via_count() == b.via_count()
def test_room_shove_baseline_contrast_with_exact():
# the exact track (no rip-up) also drops NET_C on this sealed board; the room
# engine's shove recovers it -- the concrete continuous-shove win.
dsn = parse_dsn(SHOVE_CHANNEL.read_text())
exact, _, _ = route_dsn_board_exact(dsn, rip_up=False, shove=False)
assert len(exact.routed_net_numbers) < 2
rooms, _, _ = route_dsn_board_rooms(dsn, shove=True)
assert rooms.routed_net_numbers == {1, 2}
def test_room_shove_channel_valid_ses():
ses = route(SHOVE_CHANNEL.read_text(), engine="room", 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"}
# --- gate placement: two lanes through one wide door ------------------------
def _wall_crossing_x(result, net_no, wall_y):
for _, run in result.wires[net_no]:
for a, b in zip(run, run[1:], strict=False):
if a.x == b.x and min(a.y, b.y) <= wall_y <= max(a.y, b.y):
return a.x
return None
def test_room_wide_door_seats_two_nets_at_distinct_points():
dsn = parse_dsn(WIDE_DOOR.read_text())
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
assert on.routed_net_numbers == {1, 2}
assert on.shoves == 0 # gate placement alone, no shove
assert on.drc_clean
assert on.tree.has_violation(_clearance(dsn, scale)) is None
wall_y = -30000 * scale
x1 = _wall_crossing_x(on.result, 1, wall_y)
x2 = _wall_crossing_x(on.result, 2, wall_y)
assert x1 is not None and x2 is not None
width = round(2000 * scale)
clearance = _clearance(dsn, scale)
assert abs(x1 - x2) >= width + clearance # two DISTINCT lanes
def test_room_wide_door_is_deterministic():
a, _, _ = route_dsn_board_rooms(parse_dsn(WIDE_DOOR.read_text()), shove=True)
b, _, _ = route_dsn_board_rooms(parse_dsn(WIDE_DOOR.read_text()), shove=True)
assert _wire_key(a.result) == _wire_key(b.result)
# --- DRC-clean invariant extends to the new fixtures ------------------------
@pytest.mark.parametrize("fixture", [SHOVE_CHANNEL, WIDE_DOOR])
def test_new_fixtures_drc_clean_under_shove(fixture):
dsn = parse_dsn(fixture.read_text())
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
assert on.drc_clean
assert on.tree.has_violation(_clearance(dsn, scale)) is None
# --- shove/gate placement is a no-op where it isn't needed ------------------
@pytest.mark.parametrize("fixture", FOUNDATION)
def test_shove_is_byte_identical_on_foundation_fixtures(fixture):
dsn = parse_dsn(fixture.read_text())
off, scale, _ = route_dsn_board_rooms(dsn, shove=False)
on, _, _ = route_dsn_board_rooms(dsn, shove=True)
# identical connectivity, geometry, DRC-clean, and no shove was needed
assert on.routed_net_numbers == off.routed_net_numbers
assert on.shoves == 0
assert off.drc_clean and on.drc_clean
assert on.tree.has_violation(_clearance(dsn, scale)) is None
# doors carry no other-net routed copper here, so projection == the old clamp
assert _wire_key(on.result) == _wire_key(off.result)
# --- unit coverage for the occupancy-aware projection -----------------------
def test_project_gate_reduces_to_clamp_without_occupancy():
from rpmroute.board.search_tree import ShapeSearchTree
from rpmroute.route.room_router import _Door, _edge_gate, _project_gate
door = _Door(other=1, kind="edge", lo=IntPoint(0, 100), hi=IntPoint(200, 100))
tree = ShapeSearchTree()
from_pt = IntPoint(140, 500)
plain = _edge_gate(door, from_pt, half_width=1)
projected = _project_gate(door, from_pt, 1, 2, tree, net_no=1, layer=0)
assert (projected.x, projected.y) == (plain.x, plain.y) == (140, 100)
def test_project_gate_avoids_other_net_copper_on_the_door():
from rpmroute.board.search_tree import ShapeSearchTree, TreeShape
from rpmroute.route.room_router import _Door, _project_gate
door = _Door(other=1, kind="edge", lo=IntPoint(0, 100), hi=IntPoint(200, 100))
tree = ShapeSearchTree()
# a different-net routed trace crosses the middle of the door
tree.insert(TreeShape(0, 2, frozenset({0}), IntBox(90, 50, 110, 150), routed=True))
# ideal projection is x=100, right inside the blocked band -> pushed to a free span
gate = _project_gate(door, IntPoint(100, 500), 1, 2, tree, net_no=1, layer=0)
reach = 2 + 1 # clearance + half_width
forbidden_lo, forbidden_hi = 90 - reach, 110 + reach
assert not (forbidden_lo < gate.x < forbidden_hi) # outside the open forbidden band
assert 1 <= gate.x <= 199 # inside the usable span
def test_free_intervals_splits_around_forbidden_band():
from rpmroute.route.room_router import _best_free_point, _free_intervals
free = _free_intervals(0, 200, [(87, 113)])
assert free == [(0, 87), (113, 200)]
# tie on size -> lowest-coordinate interval, clamped toward the ideal
assert _best_free_point(0, 200, [(87, 113)], ideal=100) == 87
# no forbidden band -> the whole span, clamped to the ideal (old clamp)
assert _best_free_point(0, 200, [], ideal=100) == 100
# --- regression: multi-pin wire/owner desync in shove recovery ---------------
def test_room_shove_multipin_recovery_keeps_output_drc_clean():
"""Regression for the wire/owner desync in ``_shove_recover_room``.
A 3-pin net commits one connection via ``_commit`` and has a second straight
connection *recovered* by the shove path. The recovery used to clobber the
net's ``_Accepted`` with a fresh one whose ``wire_owners`` held only the
just-recovered wire, leaving it SHORTER than the parallel ``wires`` list. A
later net's shove then removed the WRONG wire's copper from the search tree;
that copper stayed in the emitted route but vanished from the tree, so
``has_violation`` falsely certified clean while a following net was placed on
top of it -- a hard different-net short in the SES.
Assert the invariant against the *emitted* output directly, not just the tree
(the tree was the thing being fooled)."""
dsn = parse_dsn(MULTIPIN_DESYNC.read_text())
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
clearance = _clearance(dsn, scale)
# the tree self-check and the independent emitted-copper check must agree
assert on.drc_clean
assert on.tree.has_violation(clearance) is None
assert _emitted_drc_violations(on.result, clearance) == []
# the shove path actually engaged and did not silently drop the multi-pin net
assert on.shoves >= 1
assert 3 in on.routed_net_numbers
def test_room_shove_multipin_recovery_is_deterministic():
a, _, _ = route_dsn_board_rooms(parse_dsn(MULTIPIN_DESYNC.read_text()), shove=True)
b, _, _ = route_dsn_board_rooms(parse_dsn(MULTIPIN_DESYNC.read_text()), shove=True)
assert _wire_key(a.result) == _wire_key(b.result)
assert a.routed_net_numbers == b.routed_net_numbers
def test_room_shove_multipin_recovery_does_not_crash():
"""Sibling of the desync: when the conflicting wire index exceeds the
(formerly truncated) ``wire_owners`` length, the recovery raised
``IndexError`` and aborted the whole public route. Keeping ``wire_owners``
parallel to ``wires`` fixes both faces; this board must route to completion,
DRC-clean, through the public ``route(engine="room", shove=True)`` API."""
ses = route(MULTIPIN_DESYNC_CRASH.read_text(), engine="room", shove=True)
top = parse(ses)
assert top.head == "session"
# and the library path stays DRC-clean on the emitted output
dsn = parse_dsn(MULTIPIN_DESYNC_CRASH.read_text())
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
clearance = _clearance(dsn, scale)
assert on.drc_clean
assert _emitted_drc_violations(on.result, clearance) == []
# --- honest bound: shove recovers ordering drops, it does NOT create density --
def _net_blocks(text):
import re
return re.findall(r" \(net [^\n]*\n \(pins [^\n]*\n \)\n", text)
def _plain_under_all_orderings(fixture):
"""Route ``fixture`` plainly (shove=False) under every permutation of its net
declaration order, returning the routed-net set for each ordering. Net numbers
follow declaration order, so permuting the ``(net ...)`` blocks permutes the
greedy routing order -- the exact knob that turned the shove_channel drop into
an ordering artifact."""
import itertools
text = fixture.read_text()
blocks = _net_blocks(text)
joined = "".join(blocks)
assert joined in text and len(blocks) >= 2
out = []
for perm in itertools.permutations(range(len(blocks))):
variant = text.replace(joined, "".join(blocks[i] for i in perm), 1)
result, _, _ = route_dsn_board_rooms(parse_dsn(variant), shove=False)
out.append(len(result.routed_net_numbers))
return out
def test_shove_channel_plain_fits_under_some_ordering():
"""Baseline for the contrast: on shove_channel the plain drop is an ORDERING
artifact -- there exists a net order under which plain routing fits both nets
with no shove. This is exactly what makes it *not* a density limit."""
counts = _plain_under_all_orderings(SHOVE_CHANNEL)
assert max(counts) == 2 # some ordering routes both
assert min(counts) == 1 # the default order drops one
def test_true_density_plain_drops_under_every_ordering():
"""The order-independent bar: on true_density plain routing drops >= 1 net
under EVERY net ordering, unlike shove_channel. NET_C is a full-height wall on
the only signal layer, so whichever net is routed first walls off the other --
no permutation fits both. The drop is a genuine on-layer obstruction, not a
self-inflicted ordering choice."""
counts = _plain_under_all_orderings(TRUE_DENSITY)
assert len(counts) == 2 # two nets -> two orderings
assert all(k < 2 for k in counts) # every ordering drops at least one net
assert all(k == 1 for k in counts) # exactly one survives each way
def test_true_density_shove_cannot_create_space():
"""The honest result: shove does NOT recover the order-independent drop.
shove_segment RELOCATES a blocking trace into existing free space; it does not
compress copper. NET_C walls the board top-to-bottom on the sole signal layer,
so there is nowhere to relocate NET_A's crossing run to -- every perpendicular
displacement lands on NET_C or outside the board. shove therefore adds zero
shoves and routes no more nets than plain routing. This is the boundary of the
shove mechanism: it recovers ordering-induced drops (free space exists), it
cannot manufacture density (free space absent)."""
dsn = parse_dsn(TRUE_DENSITY.read_text())
off, _, _ = route_dsn_board_rooms(dsn, shove=False)
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
clearance = _clearance(dsn, scale)
assert len(off.routed_net_numbers) == 1 # plain drops one
assert on.routed_net_numbers == off.routed_net_numbers # shove recovers nothing
assert on.shoves == 0 # no clean displacement exists to try
# whatever it does route stays exactly DRC-clean (invariant never traded away)
assert on.drc_clean
assert on.tree.has_violation(clearance) is None
assert _emitted_drc_violations(on.result, clearance) == []
def test_true_density_contrast_is_only_the_wall_height():
"""Pin down *why* shove recovers shove_channel but not true_density: the two
boards share the same NET_A blocker and the same channel geometry; they differ
only in how far NET_C spans. When NET_C leaves headroom (shove_channel) shove
nudges NET_A into it and both fit; when NET_C spans the full height
(true_density) that headroom is gone and shove is powerless. The recover/fail
boundary is the free-space boundary, which is the whole point."""
short_on, _, _ = route_dsn_board_rooms(parse_dsn(SHOVE_CHANNEL.read_text()), shove=True)
wall_on, _, _ = route_dsn_board_rooms(parse_dsn(TRUE_DENSITY.read_text()), shove=True)
# space exists -> shove recovers both, with real shoves
assert short_on.routed_net_numbers == {1, 2}
assert short_on.shoves >= 1
# space absent -> shove recovers neither extra net, zero shoves
assert len(wall_on.routed_net_numbers) == 1
assert wall_on.shoves == 0
def test_true_density_is_deterministic():
a, _, _ = route_dsn_board_rooms(parse_dsn(TRUE_DENSITY.read_text()), shove=True)
b, _, _ = route_dsn_board_rooms(parse_dsn(TRUE_DENSITY.read_text()), shove=True)
assert _wire_key(a.result) == _wire_key(b.result)
assert a.routed_net_numbers == b.routed_net_numbers