Add dense fixture and shove tests
Adds shove_needed.dsn: one signal layer (no vias), where NET_A routes as a straight wall that boxes NET_C out under greedy ordering. Tests assert the contrast: without shove NET_C is dropped (result still DRC-clean, just incomplete); with shove NET_A is moved aside and both nets connect, the exact has_violation check is clean, endpoints stay on pads, and the result is deterministic. Regression tests confirm shove is a no-op on the simple/ crossing/ripup/kicad boards (same connectivity, DRC-clean, zero shoves). Oracle-gated parity is included (skips when the JAR cannot route the synthetic single-layer board).
This commit is contained in:
parent
f581f5b1e2
commit
e58f63e0f1
64
tests/dsn/fixtures/shove_needed.dsn
Normal file
64
tests/dsn/fixtures/shove_needed.dsn
Normal file
@ -0,0 +1,64 @@
|
||||
(pcb "shove_needed.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)
|
||||
)
|
||||
(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 3000 -15000 front 0)
|
||||
(place A2 197000 -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
|
||||
)
|
||||
)
|
||||
117
tests/route/test_shove.py
Normal file
117
tests/route/test_shove.py
Normal file
@ -0,0 +1,117 @@
|
||||
"""Tests for shove-and-retry on the exact-geometry track.
|
||||
|
||||
Shove moves an existing trace aside to make room for a net that would otherwise
|
||||
be dropped, without ever producing a clearance violation.
|
||||
|
||||
The dense fixture ``shove_needed.dsn`` has one signal layer (no vias): with
|
||||
greedy net ordering NET_A routes as a straight wall that boxes NET_C out, so
|
||||
without shove NET_C is dropped; with shove NET_A is moved aside and both
|
||||
connect, DRC-clean. (Rip-up would also reroute NET_A here; the shove test uses
|
||||
greedy ordering to isolate the shove mechanism.)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.dsn.sexp import parse
|
||||
from freeroute.route import route, route_dsn_board_exact
|
||||
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"
|
||||
SHOVE = FIXTURES / "shove_needed.dsn"
|
||||
REGRESSION = ["simple_2net", "crossing_2net", "ripup_needed", "kicad_routable"]
|
||||
|
||||
|
||||
def _exact(fixture, shove):
|
||||
return route_dsn_board_exact(parse_dsn(fixture.read_text()), shove=shove)
|
||||
|
||||
|
||||
# --- the headline shove contrast (greedy ordering isolates the mechanism) ----
|
||||
|
||||
|
||||
def test_without_shove_a_net_is_dropped():
|
||||
# greedy exact (no shove) cannot fit NET_C past NET_A's wall
|
||||
off, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=False)
|
||||
assert len(off.routed_net_numbers) < 2
|
||||
assert off.drc_clean # still clean, just incomplete
|
||||
|
||||
|
||||
def test_shove_connects_all_and_stays_drc_clean():
|
||||
on, scale, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
||||
assert on.routed_net_numbers == {1, 2} # both nets connected
|
||||
assert on.shoves >= 1 # at least one trace was moved aside
|
||||
assert on.drc_clean
|
||||
clearance = round(_rule_clearance_dsn(parse_dsn(SHOVE.read_text())) * scale)
|
||||
assert on.tree.has_violation(clearance) is None # exact DRC check
|
||||
|
||||
|
||||
def test_shove_result_is_deterministic():
|
||||
a, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
||||
b, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
||||
|
||||
def key(r):
|
||||
return {
|
||||
n: [(layer, [(p.x, p.y) for p in pts]) for layer, pts in segs]
|
||||
for n, segs in r.result.wires.items()
|
||||
}
|
||||
|
||||
assert key(a) == key(b)
|
||||
|
||||
|
||||
def test_shoved_trace_endpoints_still_on_pads():
|
||||
from freeroute.board import build_board
|
||||
|
||||
on, _, _ = route_dsn_board_exact(parse_dsn(SHOVE.read_text()), rip_up=False, shove=True)
|
||||
board = build_board(parse_dsn(SHOVE.read_text()))
|
||||
pins_by_net: dict[int, set[tuple[int, int]]] = {}
|
||||
for pin in board.get_pins():
|
||||
for net_no in pin.net_nos:
|
||||
pins_by_net.setdefault(net_no, set()).add((pin.location.x, pin.location.y))
|
||||
for net_no, segments in on.result.wires.items():
|
||||
pts = [(p.x, p.y) for _, seg in segments for p in seg]
|
||||
assert {pts[0], pts[-1]} == pins_by_net[net_no]
|
||||
|
||||
|
||||
def test_shove_emits_valid_ses():
|
||||
ses = route(SHOVE.read_text(), engine="exact", 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"}
|
||||
|
||||
|
||||
# --- regression: shove is a no-op where it isn't needed ----------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", REGRESSION)
|
||||
def test_shove_does_not_change_or_break_normal_boards(name):
|
||||
fixture = FIXTURES / f"{name}.dsn"
|
||||
off, _, _ = _exact(fixture, shove=False)
|
||||
on, _, _ = _exact(fixture, shove=True)
|
||||
# same connectivity, both DRC-clean, and no shove was needed
|
||||
assert on.routed_net_numbers == off.routed_net_numbers
|
||||
assert off.drc_clean and on.drc_clean
|
||||
assert on.shoves == 0
|
||||
|
||||
|
||||
# --- oracle parity ----------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.oracle
|
||||
def test_shove_connectivity_parity_with_oracle():
|
||||
from oracle import HAS_ORACLE, route_dsn, routed_net_set
|
||||
|
||||
if not HAS_ORACLE:
|
||||
pytest.skip("FreeRouting oracle unavailable")
|
||||
ours = routed_net_set(route(SHOVE.read_text(), engine="exact", shove=True))
|
||||
theirs = routed_net_set(route_dsn(SHOVE, max_passes=3, timeout=300))
|
||||
if not theirs:
|
||||
pytest.skip("reference JAR routed nothing on the synthetic single-layer board")
|
||||
assert theirs <= ours
|
||||
Loading…
x
Reference in New Issue
Block a user