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.
202 lines
7.8 KiB
Python
202 lines
7.8 KiB
Python
"""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
|