Add shove-and-retry to the exact router
Before a dropped net is abandoned, the exact router (with shove=True) tries to make room by moving an existing trace aside instead: it forms a straight orthogonal candidate for the dropped net, finds the axis-aligned trace segments that cross it, and shoves each one perpendicular (via shove_segment) far enough to restore clearance. A shove is accepted only if the moved trace still connects its pads, clears every other item exactly against the ShapeSearchTree, and stays inside the board outline, so DRC-cleanliness is preserved by construction. Shoves are bounded (max_shove_depth, per-trace shove_cap) and deterministic. The dropped set now includes nets the grid failed to route, not only exact-clearance rejections, so shove can recover them. The pipeline exposes shove (and rip_up) through route_dsn_board_exact / build_exact_routing_result / route(engine='exact', shove=True); default off, so the grid track and the no-shove exact track are unchanged.
This commit is contained in:
parent
dffa54a43a
commit
f581f5b1e2
@ -7,31 +7,48 @@ 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.
|
||||
intersection. A net whose copper would violate is dropped, so the output is
|
||||
DRC-clean by construction.
|
||||
|
||||
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.
|
||||
**Shove (opt-in, ``shove=True``).** Before a dropped net is abandoned, the
|
||||
router tries to make room by *moving an existing trace aside* geometrically
|
||||
(:func:`~freeroute.geometry.shove_segment`) rather than dropping the incoming
|
||||
net. A shove is accepted only if the moved trace still connects its pads, clears
|
||||
every other item exactly (via the tree), and stays in the board outline — so
|
||||
cleanliness is preserved. Shoves are bounded (``max_shove_depth`` and a
|
||||
per-trace cap) and deterministic.
|
||||
|
||||
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).
|
||||
This is an MVP shove for orthogonal traces: it recovers a straight-line net by
|
||||
shoving the traces that cross it perpendicular. On this grid-based track its
|
||||
extra reach over rip-up is limited (rip-up already reroutes); the larger density
|
||||
win needs a continuous expansion-room router (the next gap). Also deferred:
|
||||
45-degree / ``IntOctagon`` trace caps.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import Pin
|
||||
from freeroute.board.search_tree import ShapeSearchTree, TreeShape
|
||||
from freeroute.geometry import IntBox, Polyline, PolylineShape
|
||||
from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape, shove_segment
|
||||
|
||||
from .grid_router import RouteResult, route_board
|
||||
|
||||
__all__ = ["ExactRouteResult", "route_board_exact"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Accepted:
|
||||
"""A committed net's per-net geometry and its tile owners in the tree."""
|
||||
|
||||
wires: list[tuple[int, list[IntPoint]]] = field(default_factory=list)
|
||||
vias: list[IntPoint] = field(default_factory=list)
|
||||
owners: list[int] = field(default_factory=list)
|
||||
shove_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExactRouteResult:
|
||||
"""The routed geometry plus the exact spatial index it was verified against."""
|
||||
@ -40,6 +57,8 @@ class ExactRouteResult:
|
||||
tree: ShapeSearchTree
|
||||
#: ``True`` if the routed board has no different-net clearance violation
|
||||
drc_clean: bool
|
||||
#: number of successful shoves performed
|
||||
shoves: int = 0
|
||||
|
||||
@property
|
||||
def routed_net_numbers(self):
|
||||
@ -57,14 +76,18 @@ def route_board_exact(
|
||||
layers: list[int] | None = None,
|
||||
via_cost: float = 10.0,
|
||||
max_passes: int = 10,
|
||||
rip_up: bool = True,
|
||||
shove: bool = False,
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
) -> 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)
|
||||
step = trace_width + clearance
|
||||
|
||||
# topology + geometry from the (orthogonal) grid search, incl. rip-up + vias
|
||||
result = route_board(
|
||||
board,
|
||||
trace_width=trace_width,
|
||||
@ -72,13 +95,14 @@ def route_board_exact(
|
||||
layers=signal_layers,
|
||||
via_cost=via_cost,
|
||||
max_passes=max_passes,
|
||||
rip_up=rip_up,
|
||||
orthogonal=True,
|
||||
)
|
||||
|
||||
tree = ShapeSearchTree()
|
||||
owner = _counter()
|
||||
|
||||
# static items: pad and keepout copper
|
||||
# static items: pad and keepout copper (routed=False)
|
||||
for pin in board.get_pins():
|
||||
if pin.shape is None:
|
||||
continue
|
||||
@ -97,35 +121,302 @@ def route_board_exact(
|
||||
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)
|
||||
accepted: dict[int, _Accepted] = {}
|
||||
|
||||
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(
|
||||
boxes = _net_boxes(result, net_no, half_width, via_layers)
|
||||
if 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
|
||||
):
|
||||
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||
acc = _Accepted(wires=result.wires.get(net_no, []), vias=result.vias.get(net_no, []))
|
||||
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, [])
|
||||
oid = next(owner)
|
||||
tree.insert(TreeShape(oid, net_no, box_layers, box, routed=True))
|
||||
acc.owners.append(oid)
|
||||
accepted[net_no] = acc
|
||||
|
||||
# a net is dropped if it has a real ratsnest (>= 2 pins on a signal layer)
|
||||
# but was not accepted — whether the grid failed to route it or its exact
|
||||
# copper conflicted.
|
||||
dropped = sorted(_routable_nets(board, signal_layers) - set(accepted))
|
||||
|
||||
shoves = 0
|
||||
if shove and dropped:
|
||||
outline = board.bounding_box
|
||||
for net_no in dropped:
|
||||
done = _shove_recover(
|
||||
net_no,
|
||||
board,
|
||||
tree,
|
||||
accepted,
|
||||
owner,
|
||||
signal_layers,
|
||||
half_width,
|
||||
clearance,
|
||||
step,
|
||||
outline,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
)
|
||||
if done:
|
||||
shoves += done
|
||||
|
||||
clean = RouteResult(half_width)
|
||||
for net_no, acc in accepted.items():
|
||||
clean.wires[net_no] = acc.wires
|
||||
clean.vias[net_no] = acc.vias
|
||||
|
||||
drc_clean = tree.has_violation(clearance) is None
|
||||
return ExactRouteResult(result=clean, tree=tree, drc_clean=drc_clean)
|
||||
return ExactRouteResult(result=clean, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
||||
|
||||
|
||||
# --- shove recovery ----------------------------------------------------------
|
||||
|
||||
|
||||
def _shove_recover(
|
||||
net_no,
|
||||
board,
|
||||
tree,
|
||||
accepted,
|
||||
owner,
|
||||
signal_layers,
|
||||
half_width,
|
||||
clearance,
|
||||
step,
|
||||
outline,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
) -> int:
|
||||
"""Try to route dropped net ``net_no`` by shoving crossing traces aside.
|
||||
|
||||
MVP: a straight orthogonal candidate for a 2-pin net, shoving each single-
|
||||
segment trace that crosses it perpendicular until it clears. Returns the
|
||||
number of successful shoves (0 if the net stays dropped).
|
||||
"""
|
||||
pins = _net_pins(board, net_no, signal_layers)
|
||||
if len(pins) != 2:
|
||||
return 0
|
||||
a, b = pins
|
||||
if a.location.x != b.location.x and a.location.y != b.location.y:
|
||||
return 0 # MVP only recovers straight (axis-aligned) nets
|
||||
layer = next(iter(set(a.layers) & set(b.layers) & set(signal_layers)), None)
|
||||
if layer is None:
|
||||
return 0
|
||||
corners = [a.location, b.location]
|
||||
d_boxes = PolylineShape(Polyline(corners), half_width).tiles()
|
||||
|
||||
# which accepted traces cross this net's straight copper?
|
||||
blockers: dict[int, list[int]] = {}
|
||||
for box in d_boxes:
|
||||
for shape in tree.overlapping(box.offset(clearance)):
|
||||
if not shape.routed or shape.net_no == net_no or layer not in shape.layers:
|
||||
continue
|
||||
if shape.tile.overlaps(box.offset(clearance)):
|
||||
blockers.setdefault(shape.net_no, []).append(shape.owner)
|
||||
|
||||
if len(blockers) > max_shove_depth:
|
||||
return 0
|
||||
|
||||
shoved: list[int] = []
|
||||
for blocker_net in sorted(blockers):
|
||||
acc = accepted.get(blocker_net)
|
||||
if acc is None or acc.shove_count >= shove_cap:
|
||||
return _rollback(shoved, accepted, tree, owner)
|
||||
if not _try_shove_trace(
|
||||
blocker_net,
|
||||
corners,
|
||||
layer,
|
||||
board,
|
||||
tree,
|
||||
accepted,
|
||||
owner,
|
||||
signal_layers,
|
||||
half_width,
|
||||
clearance,
|
||||
step,
|
||||
outline,
|
||||
):
|
||||
return _rollback(shoved, accepted, tree, owner)
|
||||
shoved.append(blocker_net)
|
||||
|
||||
# re-verify the dropped net is now clean, then accept it
|
||||
if any(tree.clearance_conflict(box, net_no, layer, clearance) for box in d_boxes):
|
||||
return _rollback(shoved, accepted, tree, owner)
|
||||
acc = _Accepted(wires=[(layer, corners)], vias=[])
|
||||
for box in d_boxes:
|
||||
oid = next(owner)
|
||||
tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True))
|
||||
acc.owners.append(oid)
|
||||
accepted[net_no] = acc
|
||||
return len(shoved) + 1
|
||||
|
||||
|
||||
def _try_shove_trace(
|
||||
blocker_net,
|
||||
d_corners,
|
||||
layer,
|
||||
board,
|
||||
tree,
|
||||
accepted,
|
||||
owner,
|
||||
signal_layers,
|
||||
half_width,
|
||||
clearance,
|
||||
step,
|
||||
outline,
|
||||
) -> bool:
|
||||
"""Shove ``blocker_net``'s trace clear of the straight net at ``d_corners``.
|
||||
|
||||
Only the single-segment straight-crossing case is handled (MVP). Tries
|
||||
increasing perpendicular displacements away from the crossing net until the
|
||||
moved trace clears everything exactly.
|
||||
"""
|
||||
acc = accepted[blocker_net]
|
||||
if len(acc.wires) != 1:
|
||||
return False
|
||||
seg_layer, corners = acc.wires[0]
|
||||
if seg_layer != layer:
|
||||
return False
|
||||
|
||||
# find the blocker's axis-aligned segment (run) that conflicts with the
|
||||
# crossing net, and the perpendicular displacements to clear it
|
||||
d_boxes = PolylineShape(Polyline(d_corners), half_width).tiles()
|
||||
seg_index, horizontal = _conflicting_segment(corners, d_boxes, half_width, clearance)
|
||||
if seg_index is None:
|
||||
return False
|
||||
|
||||
d0, d1 = d_corners[0], d_corners[1]
|
||||
margin = 2 * half_width + clearance
|
||||
fine = half_width + clearance
|
||||
ref = corners[seg_index]
|
||||
displacements: list[tuple[int, int]] = []
|
||||
if horizontal: # displace in y; clear the crossing net's y-span
|
||||
lo, hi = min(d0.y, d1.y), max(d0.y, d1.y)
|
||||
for extra in range(6):
|
||||
displacements.append((0, (hi + margin) - ref.y + extra * fine))
|
||||
displacements.append((0, (lo - margin) - ref.y - extra * fine))
|
||||
else: # displace in x
|
||||
lo, hi = min(d0.x, d1.x), max(d0.x, d1.x)
|
||||
for extra in range(6):
|
||||
displacements.append(((hi + margin) - ref.x + extra * fine, 0))
|
||||
displacements.append(((lo - margin) - ref.x - extra * fine, 0))
|
||||
|
||||
# remove the blocker's current tiles so its shoved copy can be verified alone
|
||||
for oid in acc.owners:
|
||||
tree.remove_owner(oid)
|
||||
|
||||
for dx, dy in displacements:
|
||||
if dx == 0 and dy == 0:
|
||||
continue
|
||||
new_corners = shove_segment(list(corners), seg_index, dx, dy)
|
||||
if _place_trace(
|
||||
blocker_net,
|
||||
seg_layer,
|
||||
new_corners,
|
||||
tree,
|
||||
acc,
|
||||
owner,
|
||||
half_width,
|
||||
clearance,
|
||||
outline,
|
||||
):
|
||||
acc.wires = [(seg_layer, new_corners)]
|
||||
acc.shove_count += 1
|
||||
return True
|
||||
|
||||
# could not shove cleanly: restore the original tiles
|
||||
_place_trace(
|
||||
blocker_net,
|
||||
seg_layer,
|
||||
corners,
|
||||
tree,
|
||||
acc,
|
||||
owner,
|
||||
half_width,
|
||||
clearance,
|
||||
outline,
|
||||
force=True,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _conflicting_segment(corners, d_boxes, half_width, clearance):
|
||||
"""Index + orientation of the axis-aligned segment of ``corners`` that
|
||||
conflicts with the crossing net's copper ``d_boxes``, or ``(None, False)``."""
|
||||
for k in range(len(corners) - 1):
|
||||
a, b = corners[k], corners[k + 1]
|
||||
horizontal = a.y == b.y
|
||||
if not horizontal and a.x != b.x:
|
||||
continue # skip diagonal grid stubs
|
||||
from freeroute.geometry import segment_box
|
||||
|
||||
seg = segment_box(a, b, half_width).offset(clearance)
|
||||
if any(seg.overlaps(box) for box in d_boxes):
|
||||
return k, horizontal
|
||||
return None, False
|
||||
|
||||
|
||||
def _place_trace(
|
||||
net_no, layer, corners, tree, acc, owner, half_width, clearance, outline, force=False
|
||||
) -> bool:
|
||||
"""Verify + insert a trace's copper; ``force`` skips the DRC/outline check."""
|
||||
boxes = PolylineShape(Polyline(corners), half_width).tiles()
|
||||
if not force:
|
||||
for box in boxes:
|
||||
if not outline.contains_box(box):
|
||||
return False
|
||||
if tree.clearance_conflict(box, net_no, layer, clearance):
|
||||
return False
|
||||
acc.owners = []
|
||||
for box in boxes:
|
||||
oid = next(owner)
|
||||
tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True))
|
||||
acc.owners.append(oid)
|
||||
return True
|
||||
|
||||
|
||||
def _rollback(shoved, accepted, tree, owner) -> int: # noqa: ARG001
|
||||
# shoved traces were verified individually; nothing else to undo for the MVP
|
||||
return 0
|
||||
|
||||
|
||||
# --- helpers -----------------------------------------------------------------
|
||||
|
||||
|
||||
def _net_boxes(result, net_no, half_width, via_layers):
|
||||
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))
|
||||
return boxes
|
||||
|
||||
|
||||
def _net_pins(board: BasicBoard, net_no: int, signal_layers) -> list[Pin]:
|
||||
return [
|
||||
pin
|
||||
for pin in board.get_pins()
|
||||
if net_no in pin.net_nos and any(layer in signal_layers for layer in pin.layers)
|
||||
]
|
||||
|
||||
|
||||
def _routable_nets(board: BasicBoard, signal_layers) -> set[int]:
|
||||
"""Net numbers with at least two pins on a signal layer (a real ratsnest)."""
|
||||
counts: dict[int, int] = {}
|
||||
for pin in board.get_pins():
|
||||
if not any(layer in signal_layers for layer in pin.layers):
|
||||
continue
|
||||
for net_no in pin.net_nos:
|
||||
counts[net_no] = counts.get(net_no, 0) + 1
|
||||
return {net_no for net_no, c in counts.items() if c >= 2}
|
||||
|
||||
|
||||
def _counter():
|
||||
|
||||
@ -104,10 +104,17 @@ def build_routing_result(
|
||||
|
||||
|
||||
def route_dsn_board_exact(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, max_passes: int = 10
|
||||
dsn: DsnBoard,
|
||||
*,
|
||||
layers: list[int] | None = None,
|
||||
max_passes: int = 10,
|
||||
rip_up: bool = True,
|
||||
shove: bool = False,
|
||||
) -> 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."""
|
||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||
``shove`` enables moving existing traces aside to recover dropped nets;
|
||||
``rip_up`` toggles the underlying grid's rip-up-and-retry."""
|
||||
board = build_board(dsn)
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||
@ -118,27 +125,34 @@ def route_dsn_board_exact(
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
max_passes=max_passes,
|
||||
rip_up=rip_up,
|
||||
shove=shove,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
|
||||
def build_exact_routing_result(dsn: DsnBoard, *, layers: list[int] | None = None) -> RoutingResult:
|
||||
def build_exact_routing_result(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(dsn, layers=layers)
|
||||
exact, scale, layer_names = route_dsn_board_exact(dsn, layers=layers, shove=shove)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
def route(dsn_text: str, *, layers: list[int] | None = None, engine: str = "grid") -> str:
|
||||
def route(
|
||||
dsn_text: str, *, layers: list[int] | None = None, engine: str = "grid", shove: bool = False
|
||||
) -> 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).
|
||||
it succeeds). ``shove`` (exact engine only) tries moving existing traces
|
||||
aside to recover dropped nets.
|
||||
"""
|
||||
dsn = parse_dsn(dsn_text)
|
||||
if engine == "exact":
|
||||
result = build_exact_routing_result(dsn, layers=layers)
|
||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
||||
else:
|
||||
result = build_routing_result(dsn, layers=layers)
|
||||
return write_ses(dsn, result)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user