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
|
one: every item's copper is an exact :class:`~freeroute.geometry.IntBox` tile
|
||||||
indexed in a :class:`~freeroute.board.search_tree.ShapeSearchTree`, and the
|
indexed in a :class:`~freeroute.board.search_tree.ShapeSearchTree`, and the
|
||||||
routed board is verified to have **no clearance violation** by exact tile
|
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
|
**Shove (opt-in, ``shove=True``).** Before a dropped net is abandoned, the
|
||||||
value — the endpoints are the exact pad locations; interior corners are exact
|
router tries to make room by *moving an existing trace aside* geometrically
|
||||||
integer coordinates). Multi-layer and vias are preserved. The grid router stays
|
(:func:`~freeroute.geometry.shove_segment`) rather than dropping the incoming
|
||||||
the fallback; the pipeline prefers this track when it routes cleanly.
|
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
|
This is an MVP shove for orthogonal traces: it recovers a straight-line net by
|
||||||
45-degree / ``IntOctagon`` trace caps (this track routes orthogonally so the
|
shoving the traces that cross it perpendicular. On this grid-based track its
|
||||||
``IntBox`` copper is exact).
|
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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from freeroute.board.board import BasicBoard
|
from freeroute.board.board import BasicBoard
|
||||||
|
from freeroute.board.items import Pin
|
||||||
from freeroute.board.search_tree import ShapeSearchTree, TreeShape
|
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
|
from .grid_router import RouteResult, route_board
|
||||||
|
|
||||||
__all__ = ["ExactRouteResult", "route_board_exact"]
|
__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
|
@dataclass
|
||||||
class ExactRouteResult:
|
class ExactRouteResult:
|
||||||
"""The routed geometry plus the exact spatial index it was verified against."""
|
"""The routed geometry plus the exact spatial index it was verified against."""
|
||||||
@ -40,6 +57,8 @@ class ExactRouteResult:
|
|||||||
tree: ShapeSearchTree
|
tree: ShapeSearchTree
|
||||||
#: ``True`` if the routed board has no different-net clearance violation
|
#: ``True`` if the routed board has no different-net clearance violation
|
||||||
drc_clean: bool
|
drc_clean: bool
|
||||||
|
#: number of successful shoves performed
|
||||||
|
shoves: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def routed_net_numbers(self):
|
def routed_net_numbers(self):
|
||||||
@ -57,14 +76,18 @@ def route_board_exact(
|
|||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
via_cost: float = 10.0,
|
via_cost: float = 10.0,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
|
rip_up: bool = True,
|
||||||
|
shove: bool = False,
|
||||||
|
max_shove_depth: int = 5,
|
||||||
|
shove_cap: int = 3,
|
||||||
) -> ExactRouteResult:
|
) -> ExactRouteResult:
|
||||||
"""Route ``board`` orthogonally and verify the result with exact geometry."""
|
"""Route ``board`` orthogonally and verify the result with exact geometry."""
|
||||||
signal_layers = layers or [
|
signal_layers = layers or [
|
||||||
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
||||||
]
|
]
|
||||||
half_width = max(trace_width // 2, 1)
|
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(
|
result = route_board(
|
||||||
board,
|
board,
|
||||||
trace_width=trace_width,
|
trace_width=trace_width,
|
||||||
@ -72,13 +95,14 @@ def route_board_exact(
|
|||||||
layers=signal_layers,
|
layers=signal_layers,
|
||||||
via_cost=via_cost,
|
via_cost=via_cost,
|
||||||
max_passes=max_passes,
|
max_passes=max_passes,
|
||||||
|
rip_up=rip_up,
|
||||||
orthogonal=True,
|
orthogonal=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
tree = ShapeSearchTree()
|
tree = ShapeSearchTree()
|
||||||
owner = _counter()
|
owner = _counter()
|
||||||
|
|
||||||
# static items: pad and keepout copper
|
# static items: pad and keepout copper (routed=False)
|
||||||
for pin in board.get_pins():
|
for pin in board.get_pins():
|
||||||
if pin.shape is None:
|
if pin.shape is None:
|
||||||
continue
|
continue
|
||||||
@ -97,12 +121,273 @@ def route_board_exact(
|
|||||||
if not box.is_empty():
|
if not box.is_empty():
|
||||||
tree.insert(TreeShape(next(owner), -1, frozenset({obstacle.layer}), box))
|
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)
|
via_layers = frozenset(signal_layers)
|
||||||
clean = RouteResult(half_width)
|
accepted: dict[int, _Accepted] = {}
|
||||||
|
|
||||||
for net_no in sorted(set(result.wires) | set(result.vias)):
|
for net_no in sorted(set(result.wires) | set(result.vias)):
|
||||||
|
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
|
||||||
|
):
|
||||||
|
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:
|
||||||
|
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, 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]]] = []
|
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
||||||
for layer, points in result.wires.get(net_no, []):
|
for layer, points in result.wires.get(net_no, []):
|
||||||
for box in PolylineShape(Polyline(points), half_width).tiles():
|
for box in PolylineShape(Polyline(points), half_width).tiles():
|
||||||
@ -112,20 +397,26 @@ def route_board_exact(
|
|||||||
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||||
)
|
)
|
||||||
boxes.append((vbox, via_layers))
|
boxes.append((vbox, via_layers))
|
||||||
conflict = any(
|
return boxes
|
||||||
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 _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():
|
def _counter():
|
||||||
|
|||||||
@ -104,10 +104,17 @@ def build_routing_result(
|
|||||||
|
|
||||||
|
|
||||||
def route_dsn_board_exact(
|
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]]:
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
"""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)
|
board = build_board(dsn)
|
||||||
scale = max(dsn.resolution.value, 1)
|
scale = max(dsn.resolution.value, 1)
|
||||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||||
@ -118,27 +125,34 @@ def route_dsn_board_exact(
|
|||||||
clearance=clearance_board,
|
clearance=clearance_board,
|
||||||
layers=layers,
|
layers=layers,
|
||||||
max_passes=max_passes,
|
max_passes=max_passes,
|
||||||
|
rip_up=rip_up,
|
||||||
|
shove=shove,
|
||||||
)
|
)
|
||||||
return exact, scale, [layer.name for layer in dsn.layers]
|
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."""
|
"""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)
|
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.
|
"""Route a Specctra DSN string and return the routed SES string.
|
||||||
|
|
||||||
``engine`` selects the routing track: ``"grid"`` (default; the multi-layer
|
``engine`` selects the routing track: ``"grid"`` (default; the multi-layer
|
||||||
rip-up grid MVP — highest coverage, kept for backward compatibility) or
|
rip-up grid MVP — highest coverage, kept for backward compatibility) or
|
||||||
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
``"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)
|
dsn = parse_dsn(dsn_text)
|
||||||
if engine == "exact":
|
if engine == "exact":
|
||||||
result = build_exact_routing_result(dsn, layers=layers)
|
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
||||||
else:
|
else:
|
||||||
result = build_routing_result(dsn, layers=layers)
|
result = build_routing_result(dsn, layers=layers)
|
||||||
return write_ses(dsn, result)
|
return write_ses(dsn, result)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user