Prove channel packing: connects all where greedy drops, feasibility-bounded
Add channel_pack.dsn: three nets whose pads spread wider than a wall gap, so they must converge and pack into lanes. Plain room routing (shove on or off) drops >= 1 net under every net ordering; the packer connects all three under every ordering, DRC-clean (search tree and independent emitted-copper check). The falsifiable boundary test sweeps the gap width for N=3 and N=4 and asserts the packer connects all N iff the gap meets the geometric feasibility width (N-1)*(width+clearance) + 2*(half_width+clearance), never routes fewer than greedy below it, and never false-packs. Determinism, endpoints-on-pads, valid SES, and pack=False-equals-default (additive) are covered.
This commit is contained in:
parent
c28223ea9c
commit
7f81c1f4ce
75
tests/dsn/fixtures/channel_pack.dsn
Normal file
75
tests/dsn/fixtures/channel_pack.dsn
Normal file
@ -0,0 +1,75 @@
|
||||
(pcb "channel_pack.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 -80000 0 -80000 0 0)
|
||||
)
|
||||
(keepout "wall_left"
|
||||
(rect F.Cu 0 -42000 92000 -38000)
|
||||
)
|
||||
(keepout "wall_right"
|
||||
(rect F.Cu 108000 -42000 200000 -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
|
||||
(place P0a 70000 -10000 front 0)
|
||||
(place P0b 70000 -70000 front 0)
|
||||
(place P1a 100000 -10000 front 0)
|
||||
(place P1b 100000 -70000 front 0)
|
||||
(place P2a 130000 -10000 front 0)
|
||||
(place P2b 130000 -70000 front 0)
|
||||
)
|
||||
)
|
||||
(network
|
||||
(net NET0
|
||||
(pins P0a-1 P0b-1)
|
||||
)
|
||||
(net NET1
|
||||
(pins P1a-1 P1b-1)
|
||||
)
|
||||
(net NET2
|
||||
(pins P2a-1 P2b-1)
|
||||
)
|
||||
(class default
|
||||
(rule
|
||||
(width 2000)
|
||||
(clearance 2000)
|
||||
)
|
||||
)
|
||||
)
|
||||
(wiring
|
||||
)
|
||||
)
|
||||
233
tests/route/test_room_pack.py
Normal file
233
tests/route/test_room_pack.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""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 freeroute.dsn import parse_dsn
|
||||
from freeroute.dsn.sexp import parse
|
||||
from freeroute.geometry import Polyline, PolylineShape
|
||||
from freeroute.route import route, route_dsn_board_rooms
|
||||
from freeroute.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 freeroute.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
|
||||
Loading…
x
Reference in New Issue
Block a user