freeroute/tests/route/test_room_shove.py
Ryan Malloy 99c3d20de5 Prove shove-in-rooms recovers ordering drops but does not create density
Add true_density.dsn: NET_C is a full-height wall on the sole signal layer,
so plain room routing drops a net under every net ordering (an order-independent
obstruction, verified over all permutations), unlike rooms_shove_channel whose
plain drop fits under a reorder.

Test that shove recovers nothing on this board (shoves == 0, same routed set as
plain) while staying DRC-clean, and that the only difference from the recoverable
shove_channel is the wall's span. shove relocates a blocker into existing free
space; where the wall leaves no headroom there is nowhere to relocate to, so it
cannot manufacture density. The recover/fail boundary is the free-space boundary.
2026-07-13 11:34:31 -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 freeroute.board import build_board
from freeroute.dsn import parse_dsn
from freeroute.dsn.sexp import parse
from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape
from freeroute.route import route, route_dsn_board_exact, route_dsn_board_rooms
from freeroute.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 freeroute.board.search_tree import ShapeSearchTree
from freeroute.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 freeroute.board.search_tree import ShapeSearchTree, TreeShape
from freeroute.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 freeroute.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