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:
@@ -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
|
||||
Reference in New Issue
Block a user