Prove the 45-degree win: 29% shorter, DRC-clean, exact clearance load-bearing
Add diagonal_win.dsn: a 45-degree pin-to-pin net with a keepout placed inside the diagonal's bounding box but clear of the true thin copper. Orthogonal routing connects it with a Manhattan-length staircase (1600000); diagonal routing replaces that with a single 45-degree trace at the ideal hypotenuse length (1131371) -- a 29% reduction -- DRC-clean via the search tree and via an independent reconstruction of the emitted 45-degree copper's clearance octagon. test_exact_clearance_is_load_bearing shows the keepout overlaps the coarse bounding box but not the exact octagon, so the exact clearance (not the box) is what lets the diagonal route. Determinism, endpoints-on-pads, valid SES, and diagonal-off-equals-orthogonal (additive, over four fixtures) are covered.
This commit is contained in:
parent
700ed06843
commit
accec686f8
56
tests/dsn/fixtures/diagonal_win.dsn
Normal file
56
tests/dsn/fixtures/diagonal_win.dsn
Normal file
@ -0,0 +1,56 @@
|
||||
(pcb "diagonal_win.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)
|
||||
)
|
||||
)
|
||||
(boundary
|
||||
(path pcb 0 0 0 120000 0 120000 -120000 0 -120000 0 0)
|
||||
)
|
||||
(keepout "corner"
|
||||
(rect F.Cu 22000 -98000 40000 -80000)
|
||||
)
|
||||
(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 A 20000 -20000 front 0)
|
||||
(place B 100000 -100000 front 0)
|
||||
)
|
||||
)
|
||||
(network
|
||||
(net NET_A
|
||||
(pins A-1 B-1)
|
||||
)
|
||||
(class default
|
||||
(rule
|
||||
(width 2000)
|
||||
(clearance 2000)
|
||||
)
|
||||
)
|
||||
)
|
||||
(wiring
|
||||
)
|
||||
)
|
||||
201
tests/route/test_diagonal.py
Normal file
201
tests/route/test_diagonal.py
Normal file
@ -0,0 +1,201 @@
|
||||
"""Tests for 45-degree (diagonal) routing on the exact track.
|
||||
|
||||
The orthogonal exact router connects a 45-degree pin-to-pin net with a Manhattan
|
||||
staircase (length ~ dx + dy). ``diagonal=True`` replaces it with a single
|
||||
45-degree trace (length ~ hypot(dx, dy)) whose copper clearance is checked with
|
||||
the exact integer octagon, not the coarse bounding box -- so it fits past an
|
||||
obstacle that sits inside the diagonal's bounding box but clears the true thin
|
||||
copper. ``diagonal_win.dsn`` places exactly such an obstacle, making the exact
|
||||
clearance load-bearing rather than cosmetic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
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, overlaps_2d, segment_octagon
|
||||
from freeroute.route import route, route_dsn_board_exact
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
||||
DIAGONAL_WIN = FIXTURES / "diagonal_win.dsn"
|
||||
SIMPLE = FIXTURES / "simple_2net.dsn"
|
||||
CROSSING = FIXTURES / "crossing_2net.dsn"
|
||||
KICAD = FIXTURES / "kicad_routable.dsn"
|
||||
|
||||
|
||||
def _length(result):
|
||||
return sum(
|
||||
math.hypot(q.x - p.x, q.y - p.y)
|
||||
for _n, segs in result.result.wires.items()
|
||||
for _layer, pts in segs
|
||||
for p, q in zip(pts, pts[1:], strict=False)
|
||||
)
|
||||
|
||||
|
||||
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 _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
|
||||
|
||||
|
||||
def _rule_clearance_board(fixture):
|
||||
from freeroute.route.pipeline import _rule_clearance_dsn
|
||||
|
||||
dsn = parse_dsn(fixture.read_text())
|
||||
return round(_rule_clearance_dsn(dsn) * max(dsn.resolution.value, 1))
|
||||
|
||||
|
||||
# --- the falsifiable length win ----------------------------------------------
|
||||
|
||||
|
||||
def test_orthogonal_baseline_is_manhattan_length():
|
||||
"""Orthogonal (diagonal off) connects the net with a Manhattan-length route --
|
||||
the staircase the exact grid produces for a 45-degree ratsnest."""
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
ortho, _, _ = route_dsn_board_exact(dsn)
|
||||
assert ortho.routed_net_numbers == {1}
|
||||
# pins are 80000 x 80000 DSN units apart -> Manhattan = 160000, scaled x10
|
||||
assert _length(ortho) == pytest.approx(1_600_000.0)
|
||||
|
||||
|
||||
def test_diagonal_is_strictly_shorter_and_ideal():
|
||||
"""Diagonal on connects the same net with a single 45-degree trace at the ideal
|
||||
hypotenuse length -- strictly shorter than the orthogonal staircase."""
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
ortho, _, _ = route_dsn_board_exact(dsn)
|
||||
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
assert diag.routed_net_numbers == {1}
|
||||
ideal = math.hypot(800_000, 800_000) # board units
|
||||
assert _length(diag) == pytest.approx(ideal)
|
||||
assert _length(diag) < _length(ortho)
|
||||
# a real, sizeable win, not a rounding artifact
|
||||
assert _length(diag) / _length(ortho) < 0.72
|
||||
|
||||
|
||||
def test_diagonal_route_is_a_single_45_degree_segment():
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
segs = diag.result.wires[1]
|
||||
assert len(segs) == 1
|
||||
_layer, pts = segs[0]
|
||||
assert len(pts) == 2
|
||||
(ax, ay), (bx, by) = (pts[0].x, pts[0].y), (pts[1].x, pts[1].y)
|
||||
assert abs(bx - ax) == abs(by - ay) != 0 # exactly 45 degrees
|
||||
|
||||
|
||||
def test_diagonal_endpoints_stay_on_pads():
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
pins = _pins_by_net(DIAGONAL_WIN)
|
||||
for net_no, segments in diag.result.wires.items():
|
||||
pts = [(p.x, p.y) for _layer, run in segments for p in run]
|
||||
assert {pts[0], pts[-1]} == pins[net_no]
|
||||
|
||||
|
||||
# --- DRC-clean, verified exactly and independently ---------------------------
|
||||
|
||||
|
||||
def test_diagonal_is_drc_clean_via_tree():
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
clearance = _rule_clearance_board(DIAGONAL_WIN)
|
||||
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
assert diag.drc_clean
|
||||
assert diag.tree.has_violation(clearance) is None
|
||||
|
||||
|
||||
def test_diagonal_is_drc_clean_independently_of_the_tree():
|
||||
"""Reconstruct the 45-degree copper's clearance octagon from the emitted route
|
||||
and confirm it clears every different-net static copper (pads, the keepout) --
|
||||
a check that never consults the search tree the router verified against."""
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
board = build_board(dsn)
|
||||
clearance = _rule_clearance_board(DIAGONAL_WIN)
|
||||
half_width = 10_000 # trace_width 2000 * scale 10 / 2
|
||||
|
||||
diag, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
|
||||
# different-net static copper: the keepout (net -1); pads share NET_A here
|
||||
obstacles = []
|
||||
for obstacle in board.get_obstacle_areas():
|
||||
for tile in obstacle.tiles:
|
||||
obstacles.append(tile.bounding_box())
|
||||
|
||||
assert diag.result.wires # sanity: the diagonal net was actually emitted
|
||||
conflicts = 0
|
||||
for _net_no, segments in diag.result.wires.items():
|
||||
for _layer, pts in segments:
|
||||
for p, q in zip(pts, pts[1:], strict=False):
|
||||
grown = segment_octagon(p, q, half_width + clearance)
|
||||
for box in obstacles:
|
||||
if overlaps_2d(grown, box):
|
||||
conflicts += 1
|
||||
assert conflicts == 0
|
||||
|
||||
|
||||
def test_exact_clearance_is_load_bearing():
|
||||
"""The keepout sits INSIDE the diagonal's bounding box (so the coarse bbox
|
||||
clearance the plain router uses would reject the diagonal) yet clears the exact
|
||||
octagon -- proving the octagon, not the box, is what lets the trace route."""
|
||||
a, b = IntPoint(200_000, -200_000), IntPoint(1_000_000, -1_000_000)
|
||||
half_width, clearance = 10_000, 20_000
|
||||
bbox = IntBox(a.x - half_width, b.y - half_width, b.x + half_width, a.y + half_width)
|
||||
keepout = IntBox(220_000, -980_000, 400_000, -800_000)
|
||||
assert not keepout.intersection(bbox).is_empty() # inside the coarse cover
|
||||
assert not overlaps_2d(segment_octagon(a, b, half_width + clearance), keepout) # clear exactly
|
||||
|
||||
|
||||
def test_diagonal_is_deterministic():
|
||||
dsn = parse_dsn(DIAGONAL_WIN.read_text())
|
||||
a, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
b, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
assert _wire_key(a) == _wire_key(b)
|
||||
assert a.routed_net_numbers == b.routed_net_numbers
|
||||
|
||||
|
||||
def test_diagonal_valid_ses():
|
||||
ses = route(DIAGONAL_WIN.read_text(), engine="exact", diagonal=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"}
|
||||
|
||||
|
||||
# --- additive: diagonal off changes nothing on the orthogonal track ----------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", [DIAGONAL_WIN, SIMPLE, CROSSING, KICAD])
|
||||
def test_diagonal_off_matches_orthogonal(fixture):
|
||||
dsn = parse_dsn(fixture.read_text())
|
||||
base, _, _ = route_dsn_board_exact(dsn)
|
||||
off, _, _ = route_dsn_board_exact(dsn, diagonal=False)
|
||||
assert _wire_key(base) == _wire_key(off)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, KICAD])
|
||||
def test_diagonal_on_keeps_existing_fixtures_clean_and_connected(fixture):
|
||||
"""Turning diagonal on never dirties the DRC or drops a net that orthogonal
|
||||
routed -- it only ever shortens."""
|
||||
dsn = parse_dsn(fixture.read_text())
|
||||
clearance = _rule_clearance_board(fixture)
|
||||
base, _, _ = route_dsn_board_exact(dsn)
|
||||
on, _, _ = route_dsn_board_exact(dsn, diagonal=True)
|
||||
assert on.drc_clean
|
||||
assert on.tree.has_violation(clearance) is None
|
||||
assert on.routed_net_numbers >= base.routed_net_numbers
|
||||
assert _length(on) <= _length(base) + 1e-6
|
||||
Loading…
x
Reference in New Issue
Block a user