Add gate-optimal placement and shove-in-rooms to the room router
Both upgrades sit behind an opt-in shove=False flag, so the default code path is byte-for-byte the foundation router (existing room tests are unchanged by construction). With shove=True: - Gate placement becomes occupancy-aware: each edge gate is projected into the largest other-net-free sub-span of the door, with the reserved intervals read straight from the search tree. With margin = clearance + half_width the room decomposition already keeps doors clearance-clear, so this reduces exactly to the old midpoint clamp where no other-net copper crosses the door. - When a net would be dropped, a transactional shove-in-rooms recovery nudges the committed trace(s) blocking it aside with the exact shove primitive and re-verifies against the clearance oracle, so both nets fit through one channel. Every moved trace is journaled and rolled back to its exact original tiles and wires on any failure -- the room engine mutates already-committed nets, so the rollback is real rather than the exact track's no-op. The board-wide has_violation(clearance) gate stays the final assertion. Thread the flag through pipeline.route_dsn_board_rooms / build_rooms_routing_result / route(engine='room', shove=...).
This commit is contained in:
parent
e67a11c833
commit
4042f80b81
@ -141,23 +141,26 @@ def build_exact_routing_result(
|
|||||||
|
|
||||||
|
|
||||||
def route_dsn_board_rooms(
|
def route_dsn_board_rooms(
|
||||||
dsn: DsnBoard, *, layers: list[int] | None = None
|
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False
|
||||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard` with the continuous expansion-room track;
|
"""Route a parsed :class:`DsnBoard` with the continuous expansion-room track;
|
||||||
return the result, the scale, and layer names."""
|
return the result, the scale, and layer names. ``shove`` enables occupancy-
|
||||||
|
aware gate placement and shove-in-rooms recovery of otherwise-dropped nets."""
|
||||||
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)
|
||||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
rooms = route_board_rooms(
|
rooms = route_board_rooms(
|
||||||
board, trace_width=width_board, clearance=clearance_board, layers=layers
|
board, trace_width=width_board, clearance=clearance_board, layers=layers, shove=shove
|
||||||
)
|
)
|
||||||
return rooms, scale, [layer.name for layer in dsn.layers]
|
return rooms, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
|
|
||||||
def build_rooms_routing_result(dsn: DsnBoard, *, layers: list[int] | None = None) -> RoutingResult:
|
def build_rooms_routing_result(
|
||||||
|
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False
|
||||||
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (room track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (room track) and convert to a DSN-unit RoutingResult."""
|
||||||
rooms, scale, layer_names = route_dsn_board_rooms(dsn, layers=layers)
|
rooms, scale, layer_names = route_dsn_board_rooms(dsn, layers=layers, shove=shove)
|
||||||
return _to_routing_result(rooms.result, dsn, scale, layer_names)
|
return _to_routing_result(rooms.result, dsn, scale, layer_names)
|
||||||
|
|
||||||
|
|
||||||
@ -171,13 +174,14 @@ def route(
|
|||||||
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
||||||
it succeeds), or ``"room"`` (continuous expansion-room — exact free-space
|
it succeeds), or ``"room"`` (continuous expansion-room — exact free-space
|
||||||
decomposition, routes off-grid channels the grid/exact tracks cannot).
|
decomposition, routes off-grid channels the grid/exact tracks cannot).
|
||||||
``shove`` (exact engine only) tries moving existing traces aside.
|
``shove`` (exact and room engines) tries moving existing traces aside; on the
|
||||||
|
room engine it also turns on occupancy-aware gate placement.
|
||||||
"""
|
"""
|
||||||
dsn = parse_dsn(dsn_text)
|
dsn = parse_dsn(dsn_text)
|
||||||
if engine == "exact":
|
if engine == "exact":
|
||||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
||||||
elif engine == "room":
|
elif engine == "room":
|
||||||
result = build_rooms_routing_result(dsn, layers=layers)
|
result = build_rooms_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)
|
||||||
|
|||||||
@ -33,14 +33,51 @@ import heapq
|
|||||||
from freeroute.board.board import BasicBoard
|
from freeroute.board.board import BasicBoard
|
||||||
from freeroute.board.items import Pin
|
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, IntPoint, Polyline, PolylineShape
|
from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape, shove_segment
|
||||||
|
|
||||||
from .exact_router import ExactRouteResult, _routable_nets
|
from .exact_router import ExactRouteResult, _routable_nets
|
||||||
from .grid_router import RouteResult
|
from .grid_router import RouteResult
|
||||||
|
from .shove import (
|
||||||
|
conflicting_segment,
|
||||||
|
force_place_owners,
|
||||||
|
place_trace_owners,
|
||||||
|
shove_displacements,
|
||||||
|
trace_tiles,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ["route_board_rooms"]
|
__all__ = ["route_board_rooms"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Accepted:
|
||||||
|
"""A committed net's per-wire tile owners, for occupancy-aware placement and
|
||||||
|
the journaled shove rollback.
|
||||||
|
|
||||||
|
``wires`` / ``vias`` are the *same* list objects as ``RouteResult.wires`` /
|
||||||
|
``.vias`` for this net, so rewriting a shoved wire in place also updates the
|
||||||
|
routed output. ``wire_owners`` is parallel to ``wires``: each entry is the
|
||||||
|
list of :class:`~freeroute.board.search_tree.TreeShape` owner ids for that
|
||||||
|
wire's copper tiles, so a later shove can locate, remove, and roll back a
|
||||||
|
specific wire without disturbing a multi-wire net's other connections.
|
||||||
|
"""
|
||||||
|
|
||||||
|
wires: list[tuple[int, list[IntPoint]]]
|
||||||
|
vias: list[IntPoint]
|
||||||
|
wire_owners: list[list[int]] = field(default_factory=list)
|
||||||
|
via_owners: list[int] = field(default_factory=list)
|
||||||
|
shove_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _ShoveEntry:
|
||||||
|
"""One moved blocker wire, enough to restore it exactly on rollback."""
|
||||||
|
|
||||||
|
blocker_net: int
|
||||||
|
acc: _Accepted
|
||||||
|
wire_index: int
|
||||||
|
old_wire: tuple[int, list[IntPoint]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class _Room:
|
class _Room:
|
||||||
"""A convex free-space rectangle on one layer."""
|
"""A convex free-space rectangle on one layer."""
|
||||||
@ -245,11 +282,17 @@ def _dist(a: IntPoint, b: IntPoint) -> float:
|
|||||||
# --- path realization --------------------------------------------------------
|
# --- path realization --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _realize(graph, seq, start: IntPoint, goal: IntPoint, half_width):
|
def _realize(graph, seq, start: IntPoint, goal: IntPoint, half_width, occupancy=None):
|
||||||
"""Turn a room sequence into per-layer orthogonal wire segments + vias.
|
"""Turn a room sequence into per-layer orthogonal wire segments + vias.
|
||||||
|
|
||||||
Routes through each door's mid-gate with an L-connector, splitting the path
|
Routes through each door's gate with an L-connector, splitting the path at
|
||||||
at via doors. Returns ``(wires, vias)`` in board units or ``None``.
|
via doors. Returns ``(wires, vias)`` in board units or ``None``.
|
||||||
|
|
||||||
|
``occupancy`` (``None`` by default) enables occupancy-aware gate placement:
|
||||||
|
when it is ``(tree, net_no, clearance)`` each edge gate is projected into the
|
||||||
|
largest other-net-free sub-span of the door instead of the raw midpoint. With
|
||||||
|
no other-net routed copper on the door this reduces exactly to the midpoint
|
||||||
|
clamp, so the ``occupancy is None`` path stays byte-for-byte the old router.
|
||||||
"""
|
"""
|
||||||
wires: list[tuple[int, list[IntPoint]]] = []
|
wires: list[tuple[int, list[IntPoint]]] = []
|
||||||
vias: list[IntPoint] = []
|
vias: list[IntPoint] = []
|
||||||
@ -261,7 +304,11 @@ def _realize(graph, seq, start: IntPoint, goal: IntPoint, half_width):
|
|||||||
if door is None:
|
if door is None:
|
||||||
return None
|
return None
|
||||||
if door.kind == "edge":
|
if door.kind == "edge":
|
||||||
gate = _edge_gate(door, pts[-1], half_width)
|
if occupancy is None:
|
||||||
|
gate = _edge_gate(door, pts[-1], half_width)
|
||||||
|
else:
|
||||||
|
tree, net_no, clearance = occupancy
|
||||||
|
gate = _project_gate(door, pts[-1], half_width, clearance, tree, net_no, layer)
|
||||||
pts.append(gate)
|
pts.append(gate)
|
||||||
else: # via: close the current-layer wire, drop a via, start next layer
|
else: # via: close the current-layer wire, drop a via, start next layer
|
||||||
gate = _via_gate(door, pts[-1], half_width)
|
gate = _via_gate(door, pts[-1], half_width)
|
||||||
@ -296,6 +343,88 @@ def _via_gate(door: _Door, from_pt: IntPoint, half_width: int) -> IntPoint:
|
|||||||
return IntPoint(x, y)
|
return IntPoint(x, y)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_gate(
|
||||||
|
door: _Door, from_pt: IntPoint, half_width: int, clearance: int, tree, net_no: int, layer: int
|
||||||
|
) -> IntPoint:
|
||||||
|
"""An occupancy-aware gate point on an edge door.
|
||||||
|
|
||||||
|
Ports ``LocateFoundConnectionAlgo``'s projection of the current from-point
|
||||||
|
onto the door shrunk by half the trace width, but subtracts the intervals
|
||||||
|
already reserved by other-net routed copper crossing the door (the
|
||||||
|
``ExpansionDoor`` section reservations, read straight from the search tree).
|
||||||
|
The trace is seated in the largest remaining free sub-span, at the point
|
||||||
|
nearest the ideal projection (tie-break: lowest coordinate). With no other-net
|
||||||
|
routed tile on the door this returns exactly :func:`_edge_gate`'s clamp.
|
||||||
|
"""
|
||||||
|
if door.lo.x == door.hi.x: # vertical edge -> choose the y coordinate
|
||||||
|
axis_lo, axis_hi, vertical = door.lo.y, door.hi.y, True
|
||||||
|
ideal = from_pt.y
|
||||||
|
else: # horizontal edge -> choose the x coordinate
|
||||||
|
axis_lo, axis_hi, vertical = door.lo.x, door.hi.x, False
|
||||||
|
ideal = from_pt.x
|
||||||
|
lo = axis_lo + half_width
|
||||||
|
hi = axis_hi - half_width
|
||||||
|
if lo > hi: # door too narrow for the trace: same midpoint fallback as _clamp
|
||||||
|
value = (axis_lo + axis_hi) // 2
|
||||||
|
else:
|
||||||
|
ideal = max(lo, min(hi, ideal))
|
||||||
|
forbidden = _door_forbidden_intervals(
|
||||||
|
door, half_width, clearance, tree, net_no, layer, vertical
|
||||||
|
)
|
||||||
|
value = _best_free_point(lo, hi, forbidden, ideal)
|
||||||
|
return IntPoint(door.lo.x, value) if vertical else IntPoint(value, door.lo.y)
|
||||||
|
|
||||||
|
|
||||||
|
def _door_forbidden_intervals(door, half_width, clearance, tree, net_no, layer, vertical):
|
||||||
|
"""Axis intervals a trace centre must avoid: one per other-net routed tile
|
||||||
|
crossing the door edge, widened by ``clearance + half_width`` on each side."""
|
||||||
|
edge_box = IntBox(door.lo.x, door.lo.y, door.hi.x, door.hi.y)
|
||||||
|
query = edge_box.offset(clearance + half_width)
|
||||||
|
reach = clearance + half_width
|
||||||
|
forbidden: list[tuple[int, int]] = []
|
||||||
|
for shape in tree.overlapping(query):
|
||||||
|
if not shape.routed or shape.net_no == net_no or layer not in shape.layers:
|
||||||
|
continue
|
||||||
|
tile = shape.tile
|
||||||
|
if vertical:
|
||||||
|
forbidden.append((tile.ll.y - reach, tile.ur.y + reach))
|
||||||
|
else:
|
||||||
|
forbidden.append((tile.ll.x - reach, tile.ur.x + reach))
|
||||||
|
return forbidden
|
||||||
|
|
||||||
|
|
||||||
|
def _best_free_point(lo: int, hi: int, forbidden, ideal: int) -> int:
|
||||||
|
"""The point in the largest free sub-interval of ``[lo, hi]`` nearest ``ideal``.
|
||||||
|
|
||||||
|
Free sub-intervals are ``[lo, hi]`` minus the (open) ``forbidden`` intervals;
|
||||||
|
the largest is chosen (tie-break: lowest coordinate) so a trace leaves the
|
||||||
|
most room for the next one, then clamped toward ``ideal`` inside it.
|
||||||
|
"""
|
||||||
|
free = _free_intervals(lo, hi, forbidden)
|
||||||
|
if not free:
|
||||||
|
return max(lo, min(hi, ideal))
|
||||||
|
a, b = max(free, key=lambda iv: (iv[1] - iv[0], -iv[0]))
|
||||||
|
return max(a, min(b, ideal))
|
||||||
|
|
||||||
|
|
||||||
|
def _free_intervals(lo: int, hi: int, forbidden) -> list[tuple[int, int]]:
|
||||||
|
"""Closed integer sub-intervals of ``[lo, hi]`` outside every open forbidden
|
||||||
|
interval. Interval boundaries stay free: a centre exactly ``clearance`` away
|
||||||
|
from a tile is allowed by the exact clearance rule."""
|
||||||
|
free: list[tuple[int, int]] = []
|
||||||
|
cur = lo
|
||||||
|
for f_lo, f_hi in sorted(forbidden):
|
||||||
|
seg_hi = min(f_lo, hi)
|
||||||
|
if cur <= hi and seg_hi >= cur:
|
||||||
|
free.append((cur, seg_hi))
|
||||||
|
cur = max(cur, min(f_hi, hi))
|
||||||
|
if cur >= hi:
|
||||||
|
break
|
||||||
|
if cur <= hi:
|
||||||
|
free.append((cur, hi))
|
||||||
|
return free
|
||||||
|
|
||||||
|
|
||||||
def _orthogonalize(points: list[IntPoint]) -> list[IntPoint]:
|
def _orthogonalize(points: list[IntPoint]) -> list[IntPoint]:
|
||||||
"""Connect gate points with axis-aligned segments (L-shaped between each)."""
|
"""Connect gate points with axis-aligned segments (L-shaped between each)."""
|
||||||
out = [points[0]]
|
out = [points[0]]
|
||||||
@ -338,8 +467,21 @@ def route_board_rooms(
|
|||||||
clearance: int,
|
clearance: int,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
via_cost: float = 50000.0,
|
via_cost: float = 50000.0,
|
||||||
|
shove: bool = False,
|
||||||
|
max_shove_depth: int = 5,
|
||||||
|
shove_cap: int = 3,
|
||||||
) -> ExactRouteResult:
|
) -> ExactRouteResult:
|
||||||
"""Route ``board`` with the continuous expansion-room track (foundation)."""
|
"""Route ``board`` with the continuous expansion-room track.
|
||||||
|
|
||||||
|
``shove`` (opt-in) turns on two continuous, sub-cell upgrades that the grid
|
||||||
|
could not do: occupancy-aware gate placement (seat each trace at the best
|
||||||
|
point in a door, not its midpoint) and shove-in-rooms (when a net would be
|
||||||
|
dropped, nudge the blocking committed trace(s) aside with the exact shove
|
||||||
|
primitive and re-route, rolling every moved trace back on any failure). With
|
||||||
|
``shove=False`` every new branch is skipped and the output is byte-for-byte
|
||||||
|
the foundation router. ``max_shove_depth`` bounds the distinct blocker nets
|
||||||
|
per recovery; ``shove_cap`` bounds how often one trace may be moved.
|
||||||
|
"""
|
||||||
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
|
||||||
]
|
]
|
||||||
@ -369,10 +511,12 @@ def route_board_rooms(
|
|||||||
|
|
||||||
result = RouteResult(half_width)
|
result = RouteResult(half_width)
|
||||||
via_layers = frozenset(signal_layers)
|
via_layers = frozenset(signal_layers)
|
||||||
|
accepted: dict[int, _Accepted] = {}
|
||||||
|
shoves = 0
|
||||||
for net_no in sorted(_routable_nets(board, signal_layers)):
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||||
pins = _net_pins(board, net_no, signal_layers)
|
pins = _net_pins(board, net_no, signal_layers)
|
||||||
for a, b in zip(pins, pins[1:], strict=False):
|
for a, b in zip(pins, pins[1:], strict=False):
|
||||||
_route_connection(
|
shoves += _route_connection(
|
||||||
net_no,
|
net_no,
|
||||||
a,
|
a,
|
||||||
b,
|
b,
|
||||||
@ -387,10 +531,14 @@ def route_board_rooms(
|
|||||||
trace_width,
|
trace_width,
|
||||||
via_cost,
|
via_cost,
|
||||||
via_layers,
|
via_layers,
|
||||||
|
shove,
|
||||||
|
accepted,
|
||||||
|
max_shove_depth,
|
||||||
|
shove_cap,
|
||||||
)
|
)
|
||||||
|
|
||||||
drc_clean = tree.has_violation(clearance) is None
|
drc_clean = tree.has_violation(clearance) is None
|
||||||
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean)
|
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
||||||
|
|
||||||
|
|
||||||
def _route_connection(
|
def _route_connection(
|
||||||
@ -408,6 +556,10 @@ def _route_connection(
|
|||||||
trace_width,
|
trace_width,
|
||||||
via_cost,
|
via_cost,
|
||||||
via_layers,
|
via_layers,
|
||||||
|
shove=False,
|
||||||
|
accepted=None,
|
||||||
|
max_shove_depth=5,
|
||||||
|
shove_cap=3,
|
||||||
):
|
):
|
||||||
rooms_by_layer = {
|
rooms_by_layer = {
|
||||||
layer: _free_rooms(tree, outline, layer, net_no, margin, half_width)
|
layer: _free_rooms(tree, outline, layer, net_no, margin, half_width)
|
||||||
@ -418,41 +570,247 @@ def _route_connection(
|
|||||||
b_layers = [layer for layer in b.layers if layer in signal_layers]
|
b_layers = [layer for layer in b.layers if layer in signal_layers]
|
||||||
starts = _rooms_containing(graph, a.location, a_layers)
|
starts = _rooms_containing(graph, a.location, a_layers)
|
||||||
goals = _rooms_containing(graph, b.location, b_layers)
|
goals = _rooms_containing(graph, b.location, b_layers)
|
||||||
if not starts or not goals:
|
|
||||||
return
|
|
||||||
seq = _search_rooms(graph, starts, set(goals), b.location, via_cost)
|
|
||||||
if seq is None:
|
|
||||||
return
|
|
||||||
realized = _realize(graph, seq, a.location, b.location, half_width)
|
|
||||||
if realized is None:
|
|
||||||
return
|
|
||||||
wires, vias = realized
|
|
||||||
|
|
||||||
# verify every piece is exactly DRC-clean before committing
|
realized = None
|
||||||
|
if starts and goals:
|
||||||
|
seq = _search_rooms(graph, starts, set(goals), b.location, via_cost)
|
||||||
|
if seq is not None:
|
||||||
|
occupancy = (tree, net_no, clearance) if shove else None
|
||||||
|
realized = _realize(graph, seq, a.location, b.location, half_width, occupancy)
|
||||||
|
|
||||||
|
if realized is not None:
|
||||||
|
wires, vias = realized
|
||||||
|
boxes = _collect_boxes(wires, vias, half_width, via_layers, outline)
|
||||||
|
if boxes is not None and not any(
|
||||||
|
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||||
|
for box, box_layers in boxes
|
||||||
|
for layer in box_layers
|
||||||
|
):
|
||||||
|
_commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# the net would be dropped: try a transactional shove-in-rooms recovery
|
||||||
|
if shove:
|
||||||
|
return _shove_recover_room(
|
||||||
|
net_no,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
tree,
|
||||||
|
result,
|
||||||
|
accepted,
|
||||||
|
owner,
|
||||||
|
outline,
|
||||||
|
signal_layers,
|
||||||
|
half_width,
|
||||||
|
clearance,
|
||||||
|
max_shove_depth,
|
||||||
|
shove_cap,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_boxes(wires, vias, half_width, via_layers, outline):
|
||||||
|
"""The ``(box, layers)`` copper tiles of a realized route, or ``None`` if any
|
||||||
|
tile leaves the board outline (the same early-out as the old inline check)."""
|
||||||
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
||||||
for layer, corners in wires:
|
for layer, corners in wires:
|
||||||
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
||||||
if not outline.contains_box(box):
|
if not outline.contains_box(box):
|
||||||
return
|
return None
|
||||||
boxes.append((box, frozenset({layer})))
|
boxes.append((box, frozenset({layer})))
|
||||||
for loc in vias:
|
for loc in vias:
|
||||||
vbox = IntBox(
|
vbox = IntBox(
|
||||||
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))
|
||||||
if any(
|
return boxes
|
||||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
|
||||||
for box, box_layers in boxes
|
|
||||||
for layer in box_layers
|
def _commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted):
|
||||||
):
|
"""Insert a verified route's copper and record it in the routed output.
|
||||||
|
|
||||||
|
With ``shove=False`` this is the foundation router's exact commit sequence.
|
||||||
|
With ``shove=True`` the tiles are additionally grouped per wire into the
|
||||||
|
``accepted`` registry so a later recovery can move or roll back one wire.
|
||||||
|
"""
|
||||||
|
if not shove:
|
||||||
|
for layer, corners in wires:
|
||||||
|
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
||||||
|
tree.insert(TreeShape(next(owner), net_no, frozenset({layer}), box, routed=True))
|
||||||
|
for loc in vias:
|
||||||
|
vbox = IntBox(
|
||||||
|
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||||
|
)
|
||||||
|
tree.insert(TreeShape(next(owner), net_no, via_layers, vbox, routed=True))
|
||||||
|
for layer, corners in wires:
|
||||||
|
result.add_wire(net_no, layer, corners)
|
||||||
|
for loc in vias:
|
||||||
|
result.add_via(net_no, loc)
|
||||||
return
|
return
|
||||||
|
|
||||||
for box, box_layers in boxes:
|
acc = accepted.get(net_no)
|
||||||
tree.insert(TreeShape(next(owner), net_no, box_layers, box, routed=True))
|
if acc is None:
|
||||||
|
acc = _Accepted(
|
||||||
|
wires=result.wires.setdefault(net_no, []), vias=result.vias.setdefault(net_no, [])
|
||||||
|
)
|
||||||
|
accepted[net_no] = acc
|
||||||
for layer, corners in wires:
|
for layer, corners in wires:
|
||||||
|
w_owners: list[int] = []
|
||||||
|
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
||||||
|
oid = next(owner)
|
||||||
|
tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True))
|
||||||
|
w_owners.append(oid)
|
||||||
result.add_wire(net_no, layer, corners)
|
result.add_wire(net_no, layer, corners)
|
||||||
|
acc.wire_owners.append(w_owners)
|
||||||
for loc in vias:
|
for loc in vias:
|
||||||
|
vbox = IntBox(
|
||||||
|
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||||
|
)
|
||||||
|
oid = next(owner)
|
||||||
|
tree.insert(TreeShape(oid, net_no, via_layers, vbox, routed=True))
|
||||||
result.add_via(net_no, loc)
|
result.add_via(net_no, loc)
|
||||||
|
acc.via_owners.append(oid)
|
||||||
|
|
||||||
|
|
||||||
|
# --- shove-in-rooms recovery -------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _shove_recover_room(
|
||||||
|
net_no,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
tree,
|
||||||
|
result,
|
||||||
|
accepted,
|
||||||
|
owner,
|
||||||
|
outline,
|
||||||
|
signal_layers,
|
||||||
|
half_width,
|
||||||
|
clearance,
|
||||||
|
max_shove_depth,
|
||||||
|
shove_cap,
|
||||||
|
) -> int:
|
||||||
|
"""Recover a dropped net by nudging the committed trace(s) that block it.
|
||||||
|
|
||||||
|
MVP (mirrors the exact track): a straight, axis-aligned 2-pin net on one
|
||||||
|
signal layer. Its straight copper is the corridor; every routed different-net
|
||||||
|
trace crossing it is shoved perpendicular with the shared exact primitive and
|
||||||
|
re-verified against the tree, then the net is placed straight. A static tile
|
||||||
|
(pad/keepout) in the corridor, or a blocker that will not move cleanly, aborts
|
||||||
|
the whole attempt and rolls every moved trace back to its exact original
|
||||||
|
tiles and wires. Returns the number of successful moves (0 = still dropped).
|
||||||
|
"""
|
||||||
|
if a.location.x != b.location.x and a.location.y != b.location.y:
|
||||||
|
return 0 # only straight nets in this MVP
|
||||||
|
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 = trace_tiles(corners, half_width)
|
||||||
|
|
||||||
|
blockers: dict[int, set[int]] = {}
|
||||||
|
for box in d_boxes:
|
||||||
|
expanded = box.offset(clearance)
|
||||||
|
for shape in tree.overlapping(expanded):
|
||||||
|
if shape.net_no == net_no or layer not in shape.layers:
|
||||||
|
continue
|
||||||
|
if not shape.tile.overlaps(expanded):
|
||||||
|
continue
|
||||||
|
if not shape.routed:
|
||||||
|
return 0 # a pad or keepout sits in the corridor: cannot shove it
|
||||||
|
blockers.setdefault(shape.net_no, set()).add(shape.owner)
|
||||||
|
if not blockers or len(blockers) > max_shove_depth:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
journal: list[_ShoveEntry] = []
|
||||||
|
for blocker_net in sorted(blockers):
|
||||||
|
acc = accepted.get(blocker_net) if accepted is not None else None
|
||||||
|
if acc is None or acc.shove_count >= shove_cap:
|
||||||
|
_rollback_room(journal, tree, owner, half_width)
|
||||||
|
return 0
|
||||||
|
if not _shove_blocker(
|
||||||
|
blocker_net, acc, corners, layer, tree, owner, half_width, clearance, outline, journal
|
||||||
|
):
|
||||||
|
_rollback_room(journal, tree, owner, half_width)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# the corridor must now be clean and inside the outline before we commit it
|
||||||
|
if any(not outline.contains_box(box) for box in d_boxes) or any(
|
||||||
|
tree.clearance_conflict(box, net_no, layer, clearance) for box in d_boxes
|
||||||
|
):
|
||||||
|
_rollback_room(journal, tree, owner, half_width)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
owners = force_place_owners(net_no, layer, corners, tree, owner, half_width)
|
||||||
|
acc = _Accepted(
|
||||||
|
wires=result.wires.setdefault(net_no, []), vias=result.vias.setdefault(net_no, [])
|
||||||
|
)
|
||||||
|
result.add_wire(net_no, layer, corners)
|
||||||
|
acc.wire_owners.append(owners)
|
||||||
|
accepted[net_no] = acc
|
||||||
|
return len(journal) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _shove_blocker(
|
||||||
|
blocker_net, acc, d_corners, layer, tree, owner, half_width, clearance, outline, journal
|
||||||
|
) -> bool:
|
||||||
|
"""Move ``blocker_net``'s conflicting wire clear of the corridor ``d_corners``.
|
||||||
|
|
||||||
|
Finds the single wire of the blocker whose axis-aligned run crosses the
|
||||||
|
corridor, removes its tiles, and walks the perpendicular displacement ladder
|
||||||
|
until the reshaped wire places cleanly (outline + exact clearance). On success
|
||||||
|
the move is journaled for rollback; on failure the wire's original tiles are
|
||||||
|
restored and ``False`` is returned.
|
||||||
|
"""
|
||||||
|
d_boxes = trace_tiles(d_corners, half_width)
|
||||||
|
for wire_index, (seg_layer, corners) in enumerate(acc.wires):
|
||||||
|
if seg_layer != layer:
|
||||||
|
continue
|
||||||
|
seg_index, horizontal = conflicting_segment(corners, d_boxes, half_width, clearance)
|
||||||
|
if seg_index is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
old_owners = list(acc.wire_owners[wire_index])
|
||||||
|
old_wire = (seg_layer, list(corners))
|
||||||
|
for oid in old_owners:
|
||||||
|
tree.remove_owner(oid)
|
||||||
|
|
||||||
|
for dx, dy in shove_displacements(
|
||||||
|
corners, seg_index, horizontal, d_corners, half_width, clearance
|
||||||
|
):
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
continue
|
||||||
|
new_corners = shove_segment(list(corners), seg_index, dx, dy)
|
||||||
|
new_owners = place_trace_owners(
|
||||||
|
blocker_net, seg_layer, new_corners, tree, owner, half_width, clearance, outline
|
||||||
|
)
|
||||||
|
if new_owners is not None:
|
||||||
|
journal.append(_ShoveEntry(blocker_net, acc, wire_index, old_wire))
|
||||||
|
acc.wires[wire_index] = (seg_layer, new_corners)
|
||||||
|
acc.wire_owners[wire_index] = new_owners
|
||||||
|
acc.shove_count += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
# no clean displacement: restore the wire's original tiles and give up
|
||||||
|
acc.wire_owners[wire_index] = force_place_owners(
|
||||||
|
blocker_net, seg_layer, list(corners), tree, owner, half_width
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _rollback_room(journal, tree, owner, half_width) -> int:
|
||||||
|
"""Undo every journaled shove, restoring exact original tiles and wires."""
|
||||||
|
for entry in reversed(journal):
|
||||||
|
for oid in entry.acc.wire_owners[entry.wire_index]:
|
||||||
|
tree.remove_owner(oid)
|
||||||
|
layer, old_corners = entry.old_wire
|
||||||
|
entry.acc.wire_owners[entry.wire_index] = force_place_owners(
|
||||||
|
entry.blocker_net, layer, old_corners, tree, owner, half_width
|
||||||
|
)
|
||||||
|
entry.acc.wires[entry.wire_index] = (layer, old_corners)
|
||||||
|
entry.acc.shove_count -= 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _net_pins(board: BasicBoard, net_no: int, signal_layers) -> list[Pin]:
|
def _net_pins(board: BasicBoard, net_no: int, signal_layers) -> list[Pin]:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user