Add shove-in-rooms and gate-placement fixtures and tests
rooms_shove_channel is the headline density win: a sealed NET_A wall leaves no top<->bottom door, so plain room routing drops NET_C; with shove=True NET_A is nudged aside and both nets route, DRC-clean and deterministic. rooms_wide_door seats two nets through one wide gap at distinct projected gate points with no shove. Cover the occupancy-aware projection math directly, assert the four foundation fixtures stay byte-identical under shove=True (shoves==0), and extend the room DRC-clean parametrize with both new fixtures.
This commit is contained in:
parent
4042f80b81
commit
a06adbee13
70
tests/dsn/fixtures/rooms_shove_channel.dsn
Normal file
70
tests/dsn/fixtures/rooms_shove_channel.dsn
Normal file
@ -0,0 +1,70 @@
|
||||
(pcb "rooms_shove_channel.dsn"
|
||||
(parser
|
||||
(string_quote ")
|
||||
(space_in_quoted_tokens on)
|
||||
(host_cad "freeroute-test")
|
||||
(host_version "1.0")
|
||||
)
|
||||
(resolution um 10)
|
||||
(unit um)
|
||||
(structure
|
||||
(layer F.Cu
|
||||
(type signal)
|
||||
(property
|
||||
(index 0)
|
||||
)
|
||||
)
|
||||
(layer B.Cu
|
||||
(type power)
|
||||
(property
|
||||
(index 1)
|
||||
)
|
||||
)
|
||||
(boundary
|
||||
(path pcb 0 0 0 200000 0 200000 -30000 0 -30000 0 0)
|
||||
)
|
||||
(keepout "seal_left"
|
||||
(rect F.Cu 0 -30000 2500 0)
|
||||
)
|
||||
(keepout "seal_right"
|
||||
(rect F.Cu 197500 -30000 200000 0)
|
||||
)
|
||||
(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
|
||||
(place A1 7000 -15000 front 0)
|
||||
(place A2 193000 -15000 front 0)
|
||||
(place C1 100000 -6000 front 0)
|
||||
(place C2 100000 -24000 front 0)
|
||||
)
|
||||
)
|
||||
(network
|
||||
(net NET_A
|
||||
(pins A1-1 A2-1)
|
||||
)
|
||||
(net NET_C
|
||||
(pins C1-1 C2-1)
|
||||
)
|
||||
(class default
|
||||
(rule
|
||||
(width 2000)
|
||||
(clearance 2000)
|
||||
)
|
||||
)
|
||||
)
|
||||
(wiring
|
||||
)
|
||||
)
|
||||
70
tests/dsn/fixtures/rooms_wide_door.dsn
Normal file
70
tests/dsn/fixtures/rooms_wide_door.dsn
Normal file
@ -0,0 +1,70 @@
|
||||
(pcb "rooms_wide_door.dsn"
|
||||
(parser
|
||||
(string_quote ")
|
||||
(space_in_quoted_tokens on)
|
||||
(host_cad "freeroute-test")
|
||||
(host_version "1.0")
|
||||
)
|
||||
(resolution um 10)
|
||||
(unit um)
|
||||
(structure
|
||||
(layer F.Cu
|
||||
(type signal)
|
||||
(property
|
||||
(index 0)
|
||||
)
|
||||
)
|
||||
(layer B.Cu
|
||||
(type power)
|
||||
(property
|
||||
(index 1)
|
||||
)
|
||||
)
|
||||
(boundary
|
||||
(path pcb 0 0 0 200000 0 200000 -60000 0 -60000 0 0)
|
||||
)
|
||||
(keepout "wall_left"
|
||||
(rect F.Cu 0 -32000 90000 -28000)
|
||||
)
|
||||
(keepout "wall_right"
|
||||
(rect F.Cu 110000 -32000 200000 -28000)
|
||||
)
|
||||
(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
|
||||
(place A1 98000 -10000 front 0)
|
||||
(place A2 98000 -50000 front 0)
|
||||
(place B1 102000 -10000 front 0)
|
||||
(place B2 102000 -50000 front 0)
|
||||
)
|
||||
)
|
||||
(network
|
||||
(net NET_A
|
||||
(pins A1-1 A2-1)
|
||||
)
|
||||
(net NET_B
|
||||
(pins B1-1 B2-1)
|
||||
)
|
||||
(class default
|
||||
(rule
|
||||
(width 2000)
|
||||
(clearance 2000)
|
||||
)
|
||||
)
|
||||
)
|
||||
(wiring
|
||||
)
|
||||
)
|
||||
@ -26,6 +26,8 @@ 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):
|
||||
@ -37,7 +39,7 @@ def _clearance(dsn, scale):
|
||||
# --- DRC-clean invariant (same non-negotiable as the exact track) -----------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, KICAD, NARROW])
|
||||
@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)
|
||||
|
||||
228
tests/route/test_room_shove.py
Normal file
228
tests/route/test_room_shove.py
Normal file
@ -0,0 +1,228 @@
|
||||
"""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** is the real continuous, sub-cell win the grid could not do:
|
||||
when a net would be dropped, the committed trace blocking it is nudged aside
|
||||
with the exact shove primitive so *both* fit through the same channel. The
|
||||
headline fixture ``rooms_shove_channel`` drops a net without it and routes both
|
||||
with it, DRC-clean.
|
||||
"""
|
||||
|
||||
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
|
||||
from freeroute.route import route, route_dsn_board_exact, route_dsn_board_rooms
|
||||
from freeroute.route.pipeline import _rule_clearance_dsn
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
|
||||
# --- the headline density win -----------------------------------------------
|
||||
|
||||
|
||||
def test_room_shove_channel_density_win():
|
||||
dsn = parse_dsn(SHOVE_CHANNEL.read_text())
|
||||
off, _, _ = route_dsn_board_rooms(dsn, shove=False)
|
||||
on, scale, _ = route_dsn_board_rooms(dsn, shove=True)
|
||||
|
||||
# without shove NET_A's sealed wall leaves no top<->bottom door -> 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_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
|
||||
Loading…
x
Reference in New Issue
Block a user