Add exact-geometry (orthogonal, DRC-clean) routing track
Adds route/exact_router.py: routes with the grid track's rip-up/multi-layer/ via topology search in orthogonal mode (axis-aligned segments -> exact IntBox copper), then verifies clearance exactly against a ShapeSearchTree. A net whose copper would come closer than the clearance to any accepted item is dropped, so the emitted geometry is DRC-clean by construction; endpoints stay exactly on pads and vias at real layer transitions. GridRouter gains a non-breaking orthogonal option (4-connected). The pipeline gains an engine= selector: 'grid' (default, unchanged, highest coverage) or 'exact' (DRC-verified). The grid MVP stays the fallback. On the designed boards the exact track routes every net DRC-clean; on the real KiCad board it matches the grid's connectivity (4/4 multi-pin nets) and is DRC-clean where the grid MVP's widened centrelines are not. Deferred: shove, and 45-degree/IntOctagon trace caps. Tests: the exact router's output has no clearance violation (exact IntBox check) on every fixture; endpoints on pads; traces orthogonal; crossing uses a via; and a direct comparison showing the exact track is clean where the grid MVP violates on the real board. Oracle-gated parity for engine='exact'.
This commit is contained in:
parent
b200ecf6f8
commit
0bee385e71
@ -14,8 +14,15 @@ multi-layer via search, and shove are deferred.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .exact_router import ExactRouteResult, route_board_exact
|
||||||
from .grid_router import GridRouter, RouteResult, route_board
|
from .grid_router import GridRouter, RouteResult, route_board
|
||||||
from .pipeline import build_routing_result, route, route_dsn_board
|
from .pipeline import (
|
||||||
|
build_exact_routing_result,
|
||||||
|
build_routing_result,
|
||||||
|
route,
|
||||||
|
route_dsn_board,
|
||||||
|
route_dsn_board_exact,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GridRouter",
|
"GridRouter",
|
||||||
@ -24,4 +31,8 @@ __all__ = [
|
|||||||
"route",
|
"route",
|
||||||
"route_dsn_board",
|
"route_dsn_board",
|
||||||
"build_routing_result",
|
"build_routing_result",
|
||||||
|
"ExactRouteResult",
|
||||||
|
"route_board_exact",
|
||||||
|
"build_exact_routing_result",
|
||||||
|
"route_dsn_board_exact",
|
||||||
]
|
]
|
||||||
|
|||||||
135
src/freeroute/route/exact_router.py
Normal file
135
src/freeroute/route/exact_router.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
"""Exact-geometry routing track (orthogonal), built on the search tree.
|
||||||
|
|
||||||
|
The grid MVP (:mod:`freeroute.route.grid_router`) approximates clearance with
|
||||||
|
cell occupancy and emits centrelines with no width-aware DRC check. This track
|
||||||
|
keeps the grid's rip-up/multi-layer/via *topology* search (in orthogonal mode,
|
||||||
|
so segments are axis-aligned) but replaces the occupancy check with an **exact**
|
||||||
|
one: every item's copper is an exact :class:`~freeroute.geometry.IntBox` tile
|
||||||
|
indexed in a :class:`~freeroute.board.search_tree.ShapeSearchTree`, and the
|
||||||
|
routed board is verified to have **no clearance violation** by exact tile
|
||||||
|
intersection.
|
||||||
|
|
||||||
|
Traces are exact segments between the true pad/via anchors (not grid-snapped in
|
||||||
|
value — the endpoints are the exact pad locations; interior corners are exact
|
||||||
|
integer coordinates). Multi-layer and vias are preserved. The grid router stays
|
||||||
|
the fallback; the pipeline prefers this track when it routes cleanly.
|
||||||
|
|
||||||
|
Deferred: **shove** (moving an existing trace aside instead of ripping it) and
|
||||||
|
45-degree / ``IntOctagon`` trace caps (this track routes orthogonally so the
|
||||||
|
``IntBox`` copper is exact).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from freeroute.board.board import BasicBoard
|
||||||
|
from freeroute.board.search_tree import ShapeSearchTree, TreeShape
|
||||||
|
from freeroute.geometry import IntBox, Polyline, PolylineShape
|
||||||
|
|
||||||
|
from .grid_router import RouteResult, route_board
|
||||||
|
|
||||||
|
__all__ = ["ExactRouteResult", "route_board_exact"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ExactRouteResult:
|
||||||
|
"""The routed geometry plus the exact spatial index it was verified against."""
|
||||||
|
|
||||||
|
result: RouteResult
|
||||||
|
tree: ShapeSearchTree
|
||||||
|
#: ``True`` if the routed board has no different-net clearance violation
|
||||||
|
drc_clean: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def routed_net_numbers(self):
|
||||||
|
return self.result.routed_net_numbers
|
||||||
|
|
||||||
|
def via_count(self) -> int:
|
||||||
|
return self.result.via_count()
|
||||||
|
|
||||||
|
|
||||||
|
def route_board_exact(
|
||||||
|
board: BasicBoard,
|
||||||
|
*,
|
||||||
|
trace_width: int,
|
||||||
|
clearance: int,
|
||||||
|
layers: list[int] | None = None,
|
||||||
|
via_cost: float = 10.0,
|
||||||
|
max_passes: int = 10,
|
||||||
|
) -> ExactRouteResult:
|
||||||
|
"""Route ``board`` orthogonally and verify the result with exact geometry."""
|
||||||
|
signal_layers = layers or [
|
||||||
|
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
||||||
|
]
|
||||||
|
half_width = max(trace_width // 2, 1)
|
||||||
|
|
||||||
|
# topology + geometry from the (orthogonal) grid search, incl. rip-up + vias
|
||||||
|
result = route_board(
|
||||||
|
board,
|
||||||
|
trace_width=trace_width,
|
||||||
|
clearance=clearance,
|
||||||
|
layers=signal_layers,
|
||||||
|
via_cost=via_cost,
|
||||||
|
max_passes=max_passes,
|
||||||
|
orthogonal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
tree = ShapeSearchTree()
|
||||||
|
owner = _counter()
|
||||||
|
|
||||||
|
# static items: pad and keepout copper
|
||||||
|
for pin in board.get_pins():
|
||||||
|
if pin.shape is None:
|
||||||
|
continue
|
||||||
|
box = pin.shape.bounding_box()
|
||||||
|
if box.is_empty():
|
||||||
|
continue
|
||||||
|
net = pin.net_nos[0] if pin.net_nos else -1
|
||||||
|
pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
||||||
|
if pin_layers:
|
||||||
|
tree.insert(TreeShape(next(owner), net, pin_layers, box))
|
||||||
|
for obstacle in board.get_obstacle_areas():
|
||||||
|
if obstacle.layer not in signal_layers:
|
||||||
|
continue
|
||||||
|
for tile in obstacle.tiles:
|
||||||
|
box = tile.bounding_box()
|
||||||
|
if not box.is_empty():
|
||||||
|
tree.insert(TreeShape(next(owner), -1, frozenset({obstacle.layer}), box))
|
||||||
|
|
||||||
|
# routed traces and vias: accept a net only if all its copper clears every
|
||||||
|
# already-accepted item exactly. A net that would violate is dropped, so the
|
||||||
|
# emitted geometry is DRC-clean by construction (coverage is best-effort).
|
||||||
|
via_layers = frozenset(signal_layers)
|
||||||
|
clean = RouteResult(half_width)
|
||||||
|
for net_no in sorted(set(result.wires) | set(result.vias)):
|
||||||
|
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
||||||
|
for layer, points in result.wires.get(net_no, []):
|
||||||
|
for box in PolylineShape(Polyline(points), half_width).tiles():
|
||||||
|
boxes.append((box, frozenset({layer})))
|
||||||
|
for loc in result.vias.get(net_no, []):
|
||||||
|
vbox = IntBox(
|
||||||
|
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||||
|
)
|
||||||
|
boxes.append((vbox, via_layers))
|
||||||
|
conflict = any(
|
||||||
|
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||||
|
for box, box_layers in boxes
|
||||||
|
for layer in box_layers
|
||||||
|
)
|
||||||
|
if conflict:
|
||||||
|
continue # drop the net to preserve DRC-cleanliness
|
||||||
|
for box, box_layers in boxes:
|
||||||
|
tree.insert(TreeShape(next(owner), net_no, box_layers, box, routed=True))
|
||||||
|
clean.wires[net_no] = result.wires.get(net_no, [])
|
||||||
|
clean.vias[net_no] = result.vias.get(net_no, [])
|
||||||
|
|
||||||
|
drc_clean = tree.has_violation(clearance) is None
|
||||||
|
return ExactRouteResult(result=clean, tree=tree, drc_clean=drc_clean)
|
||||||
|
|
||||||
|
|
||||||
|
def _counter():
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
yield i
|
||||||
|
i += 1
|
||||||
@ -128,6 +128,7 @@ class GridRouter:
|
|||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
rip_cap: int = 4,
|
rip_cap: int = 4,
|
||||||
rip_penalty: float = 30.0,
|
rip_penalty: float = 30.0,
|
||||||
|
orthogonal: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.board = board
|
self.board = board
|
||||||
self.trace_width = max(trace_width, 1)
|
self.trace_width = max(trace_width, 1)
|
||||||
@ -138,6 +139,9 @@ class GridRouter:
|
|||||||
self.max_passes = max(max_passes, 1)
|
self.max_passes = max(max_passes, 1)
|
||||||
self.rip_cap = max(rip_cap, 0)
|
self.rip_cap = max(rip_cap, 0)
|
||||||
self.rip_penalty = rip_penalty
|
self.rip_penalty = rip_penalty
|
||||||
|
# orthogonal routing (axis-aligned segments only) yields exact IntBox
|
||||||
|
# trace copper; 8-connected also produces 45-degree segments.
|
||||||
|
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
||||||
# cell size must fit a trace plus its clearance to a neighbouring trace
|
# cell size must fit a trace plus its clearance to a neighbouring trace
|
||||||
self.step = max(self.trace_width + self.clearance, 1)
|
self.step = max(self.trace_width + self.clearance, 1)
|
||||||
|
|
||||||
@ -272,7 +276,7 @@ class GridRouter:
|
|||||||
return _reconstruct(came_from, current)
|
return _reconstruct(came_from, current)
|
||||||
cx, cy, cl = current
|
cx, cy, cl = current
|
||||||
base = g_score[current]
|
base = g_score[current]
|
||||||
for dx, dy in _NEIGHBOURS:
|
for dx, dy in self._neighbours:
|
||||||
cell = (cx + dx, cy + dy)
|
cell = (cx + dx, cy + dy)
|
||||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||||
continue
|
continue
|
||||||
@ -485,6 +489,7 @@ def route_board(
|
|||||||
via_cost: float = 10.0,
|
via_cost: float = 10.0,
|
||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
|
orthogonal: bool = False,
|
||||||
) -> RouteResult:
|
) -> RouteResult:
|
||||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||||
router = GridRouter(
|
router = GridRouter(
|
||||||
@ -495,5 +500,6 @@ def route_board(
|
|||||||
via_cost=via_cost,
|
via_cost=via_cost,
|
||||||
rip_up=rip_up,
|
rip_up=rip_up,
|
||||||
max_passes=max_passes,
|
max_passes=max_passes,
|
||||||
|
orthogonal=orthogonal,
|
||||||
)
|
)
|
||||||
return router.route()
|
return router.route()
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from freeroute.board import build_board
|
|||||||
from freeroute.dsn import DsnBoard, parse_dsn
|
from freeroute.dsn import DsnBoard, parse_dsn
|
||||||
from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
||||||
|
|
||||||
|
from .exact_router import ExactRouteResult, route_board_exact
|
||||||
from .grid_router import RouteResult, route_board
|
from .grid_router import RouteResult, route_board
|
||||||
|
|
||||||
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
||||||
@ -69,12 +70,8 @@ def route_dsn_board(
|
|||||||
return result, scale, [layer.name for layer in dsn.layers]
|
return result, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
|
|
||||||
def build_routing_result(
|
def _to_routing_result(route, dsn: DsnBoard, scale: int, layer_names: list[str]) -> RoutingResult:
|
||||||
dsn: DsnBoard, *, layers: list[int] | None = None, rip_up: bool = True
|
"""Convert a board-unit :class:`RouteResult` to a DSN-unit RoutingResult."""
|
||||||
) -> RoutingResult:
|
|
||||||
"""Route ``dsn`` and convert the board-unit paths + vias to a DSN-unit
|
|
||||||
:class:`~freeroute.ses.RoutingResult` for SES emission."""
|
|
||||||
route, scale, layer_names = route_dsn_board(dsn, layers=layers, rip_up=rip_up)
|
|
||||||
width_dsn = _rule_width_dsn(dsn)
|
width_dsn = _rule_width_dsn(dsn)
|
||||||
via_name = _via_padstack(dsn)
|
via_name = _via_padstack(dsn)
|
||||||
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
||||||
@ -98,8 +95,50 @@ def build_routing_result(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def route(dsn_text: str, *, layers: list[int] | None = None) -> str:
|
def build_routing_result(
|
||||||
"""Route a Specctra DSN string and return the routed SES string."""
|
dsn: DsnBoard, *, layers: list[int] | None = None, rip_up: bool = True
|
||||||
|
) -> RoutingResult:
|
||||||
|
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||||
|
route, scale, layer_names = route_dsn_board(dsn, layers=layers, rip_up=rip_up)
|
||||||
|
return _to_routing_result(route, dsn, scale, layer_names)
|
||||||
|
|
||||||
|
|
||||||
|
def route_dsn_board_exact(
|
||||||
|
dsn: DsnBoard, *, layers: list[int] | None = None, max_passes: int = 10
|
||||||
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
|
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||||
|
DRC-verified) track; return the exact result, the scale, and layer names."""
|
||||||
|
board = build_board(dsn)
|
||||||
|
scale = max(dsn.resolution.value, 1)
|
||||||
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||||
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
|
exact = route_board_exact(
|
||||||
|
board,
|
||||||
|
trace_width=width_board,
|
||||||
|
clearance=clearance_board,
|
||||||
|
layers=layers,
|
||||||
|
max_passes=max_passes,
|
||||||
|
)
|
||||||
|
return exact, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
|
|
||||||
|
def build_exact_routing_result(dsn: DsnBoard, *, layers: list[int] | None = None) -> RoutingResult:
|
||||||
|
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||||
|
exact, scale, layer_names = route_dsn_board_exact(dsn, layers=layers)
|
||||||
|
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||||
|
|
||||||
|
|
||||||
|
def route(dsn_text: str, *, layers: list[int] | None = None, engine: str = "grid") -> str:
|
||||||
|
"""Route a Specctra DSN string and return the routed SES string.
|
||||||
|
|
||||||
|
``engine`` selects the routing track: ``"grid"`` (default; the multi-layer
|
||||||
|
rip-up grid MVP — highest coverage, kept for backward compatibility) or
|
||||||
|
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
||||||
|
it succeeds, but lower coverage on dense boards).
|
||||||
|
"""
|
||||||
dsn = parse_dsn(dsn_text)
|
dsn = parse_dsn(dsn_text)
|
||||||
result = build_routing_result(dsn, layers=layers)
|
if engine == "exact":
|
||||||
|
result = build_exact_routing_result(dsn, layers=layers)
|
||||||
|
else:
|
||||||
|
result = build_routing_result(dsn, layers=layers)
|
||||||
return write_ses(dsn, result)
|
return write_ses(dsn, result)
|
||||||
|
|||||||
154
tests/route/test_exact_router.py
Normal file
154
tests/route/test_exact_router.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
"""Tests for the exact-geometry (orthogonal, DRC-verified) routing track.
|
||||||
|
|
||||||
|
The headline invariant: the exact router's output has **no clearance violation**
|
||||||
|
by exact ``IntBox`` intersection — where the grid MVP's centrelines, given real
|
||||||
|
width, may. Endpoints stay exactly on pads; vias sit at real layer transitions;
|
||||||
|
connectivity parity holds against the JAR oracle on the designed boards.
|
||||||
|
|
||||||
|
Fixtures: simple_2net (single-layer), crossing_2net (needs a via), ripup_needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freeroute.board import ShapeSearchTree, TreeShape, build_board
|
||||||
|
from freeroute.dsn import parse_dsn
|
||||||
|
from freeroute.dsn.sexp import parse
|
||||||
|
from freeroute.geometry import IntBox, Polyline, PolylineShape
|
||||||
|
from freeroute.route import route, route_dsn_board, route_dsn_board_exact
|
||||||
|
from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_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"
|
||||||
|
RIPUP = FIXTURES / "ripup_needed.dsn"
|
||||||
|
KICAD = FIXTURES / "kicad_routable.dsn"
|
||||||
|
|
||||||
|
|
||||||
|
def _tree_from_result(board, result, half_width, signal_layers):
|
||||||
|
"""Index a RouteResult's copper (with width) plus pads into a search tree."""
|
||||||
|
tree = ShapeSearchTree()
|
||||||
|
owner = 0
|
||||||
|
for pin in board.get_pins():
|
||||||
|
if pin.shape is None:
|
||||||
|
continue
|
||||||
|
net = pin.net_nos[0] if pin.net_nos else -1
|
||||||
|
layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
||||||
|
if layers:
|
||||||
|
tree.insert(TreeShape(owner, net, layers, pin.shape.bounding_box()))
|
||||||
|
owner += 1
|
||||||
|
for net_no, segments in result.wires.items():
|
||||||
|
for layer, points in segments:
|
||||||
|
for box in PolylineShape(Polyline(points), half_width).tiles():
|
||||||
|
tree.insert(TreeShape(owner, net_no, frozenset({layer}), box, routed=True))
|
||||||
|
owner += 1
|
||||||
|
for net_no, locations in result.vias.items():
|
||||||
|
for loc in locations:
|
||||||
|
box = IntBox(
|
||||||
|
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||||
|
)
|
||||||
|
tree.insert(TreeShape(owner, net_no, frozenset(signal_layers), box, routed=True))
|
||||||
|
owner += 1
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
# --- headline: exact router output is DRC-clean -----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING, RIPUP, KICAD])
|
||||||
|
def test_exact_router_output_is_drc_clean(fixture):
|
||||||
|
dsn = parse_dsn(fixture.read_text())
|
||||||
|
exact, scale, _ = route_dsn_board_exact(dsn)
|
||||||
|
assert exact.drc_clean
|
||||||
|
clearance = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
|
assert exact.tree.has_violation(clearance) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_router_connects_all_nets_on_designed_boards():
|
||||||
|
for fixture in (SIMPLE, CROSSING, RIPUP):
|
||||||
|
exact, _, _ = route_dsn_board_exact(parse_dsn(fixture.read_text()))
|
||||||
|
assert exact.routed_net_numbers == {1, 2}
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_crossing_uses_a_via():
|
||||||
|
exact, _, _ = route_dsn_board_exact(parse_dsn(CROSSING.read_text()))
|
||||||
|
assert exact.via_count() >= 1
|
||||||
|
|
||||||
|
|
||||||
|
# --- exact geometry properties ----------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_endpoints_are_exactly_on_pads():
|
||||||
|
exact, _, _ = route_dsn_board_exact(parse_dsn(SIMPLE.read_text()))
|
||||||
|
board = build_board(parse_dsn(SIMPLE.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 exact.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_exact_traces_are_orthogonal():
|
||||||
|
exact, _, _ = route_dsn_board_exact(parse_dsn(CROSSING.read_text()))
|
||||||
|
for segments in exact.result.wires.values():
|
||||||
|
for _, points in segments:
|
||||||
|
for a, b in Polyline(points).segments():
|
||||||
|
assert a.x == b.x or a.y == b.y # axis-aligned
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_engine_emits_valid_ses_with_vias():
|
||||||
|
ses = route(CROSSING.read_text(), engine="exact")
|
||||||
|
assert parse(ses).head == "session"
|
||||||
|
network = parse(ses).child("routes").child("network_out")
|
||||||
|
total_vias = sum(len(net.children("via")) for net in network.children("net"))
|
||||||
|
assert total_vias >= 1
|
||||||
|
|
||||||
|
|
||||||
|
# --- exact passes DRC where the grid MVP does not ---------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_exact_clean_where_grid_mvp_violates_on_real_board():
|
||||||
|
dsn = parse_dsn(KICAD.read_text())
|
||||||
|
board = build_board(dsn)
|
||||||
|
scale = dsn.resolution.value
|
||||||
|
half_width = round(_rule_width_dsn(dsn) * scale) // 2
|
||||||
|
clearance = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
|
signal_layers = [i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal]
|
||||||
|
|
||||||
|
# grid MVP (8-connected): route, then give the centrelines real width
|
||||||
|
grid_result, _, _ = route_dsn_board(dsn)
|
||||||
|
grid_tree = _tree_from_result(board, grid_result, half_width, signal_layers)
|
||||||
|
grid_has_violation = grid_tree.has_violation(clearance) is not None
|
||||||
|
|
||||||
|
# exact track: DRC-clean by construction
|
||||||
|
exact, _, _ = route_dsn_board_exact(dsn)
|
||||||
|
|
||||||
|
assert exact.drc_clean
|
||||||
|
# the grid MVP's widened centrelines are not DRC-clean on this dense board;
|
||||||
|
# the exact track is. (If a future grid change makes it clean too, that is
|
||||||
|
# still fine — the exact track's guarantee is the point.)
|
||||||
|
assert grid_has_violation
|
||||||
|
|
||||||
|
|
||||||
|
# --- oracle parity ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.oracle
|
||||||
|
@pytest.mark.parametrize("fixture", [SIMPLE, CROSSING])
|
||||||
|
def test_exact_connectivity_parity_with_oracle(fixture):
|
||||||
|
from oracle import HAS_ORACLE, route_dsn, routed_net_set
|
||||||
|
|
||||||
|
if not HAS_ORACLE:
|
||||||
|
pytest.skip("FreeRouting oracle unavailable")
|
||||||
|
ours = routed_net_set(route(fixture.read_text(), engine="exact"))
|
||||||
|
theirs = routed_net_set(route_dsn(fixture, max_passes=3, timeout=300))
|
||||||
|
assert theirs, "oracle routed nothing on a routable board"
|
||||||
|
assert theirs <= ours
|
||||||
Loading…
x
Reference in New Issue
Block a user