Files
rpmroute/src/freeroute/route/room_router.py
T
rsp2k 4042f80b81 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=...).
2026-07-13 10:27:10 -06:00

829 lines
31 KiB
Python

"""Continuous expansion-room routing track (foundation).
The grid and exact tracks quantize space into fixed ``width + clearance`` cells.
This track replaces that grid with FreeRouting's idea of **free-space rooms**:
per layer, the space *not* occupied by items (inflated by the routing clearance)
is decomposed into exact convex tiles (here axis-aligned :class:`IntBox` rooms
via coordinate compression, the orthogonal analogue of
``CompleteFreeSpaceExpansionRoom``). Adjacent rooms share a **door** on their
common edge (and a **via door** joins the same footprint on two layers); the
maze search is an A* over rooms-through-doors, so a path may cross a door at any
point along its width — continuous, not cell-quantized. The found room sequence
is realized into an exact orthogonal :class:`Polyline` and verified DRC-clean
against the :class:`ShapeSearchTree`, exactly like the exact track.
**Foundation scope (this is large — the tail is deferred):** rooms are
axis-aligned rectangles (not general ``Simplex`` tiles); path realization routes
through door mid-gates with an L-connector and drops a net if the realized trace
cannot be made exactly clean. Any-angle rooms, optimal gate placement (true
sub-cell shove), and optimization passes are follow-ups. The grid and exact
tracks are unchanged; this is opt-in via ``engine="room"``.
Maps to FreeRouting: rooms ← ``ExpansionRoom`` /
``CompleteFreeSpaceExpansionRoom``; doors ← ``ExpansionDoor`` /
``TargetItemExpansionDoor``; search ← ``MazeSearchAlgo`` / ``AutorouteEngine``;
realization ← ``LocateFoundConnectionAlgo``.
"""
from __future__ import annotations
from dataclasses import dataclass, field
import heapq
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, IntPoint, Polyline, PolylineShape, shove_segment
from .exact_router import ExactRouteResult, _routable_nets
from .grid_router import RouteResult
from .shove import (
conflicting_segment,
force_place_owners,
place_trace_owners,
shove_displacements,
trace_tiles,
)
__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)
class _Room:
"""A convex free-space rectangle on one layer."""
id: int
layer: int
box: IntBox
def center(self) -> IntPoint:
return IntPoint((self.box.ll.x + self.box.ur.x) // 2, (self.box.ll.y + self.box.ur.y) // 2)
@dataclass
class _Door:
"""A passage between two rooms: a shared edge, or a via between layers."""
other: int # neighbouring room id
kind: str # "edge" | "via"
# gate geometry: for an edge door the shared segment, for a via the overlap box
lo: IntPoint
hi: IntPoint
@dataclass
class _Graph:
rooms: dict[int, _Room] = field(default_factory=dict)
doors: dict[int, list[_Door]] = field(default_factory=dict)
by_layer: dict[int, list[int]] = field(default_factory=dict)
# --- free-space decomposition ------------------------------------------------
def _free_rooms(
tree: ShapeSearchTree, outline: IntBox, layer: int, net_no: int, margin: int, half_width: int
) -> list[IntBox]:
"""Decompose the centreline-free space on ``layer`` into rectangles.
Obstacles are every other-net tile on this layer, inflated by ``margin``
(clearance + half-width) so a centreline inside a room keeps the trace clear.
The routing region is the outline shrunk by ``half_width``.
"""
region = outline.offset(-half_width)
if region.is_empty():
return []
obstacles: list[IntBox] = []
for shape in tree.all_shapes():
if net_no in (shape.net_no,) or layer not in shape.layers:
continue
infl = shape.tile.offset(margin)
clipped = infl.intersection(region)
if not clipped.is_empty() and clipped.dimension() == 2:
obstacles.append(clipped)
xs = sorted(
{region.ll.x, region.ur.x} | {o.ll.x for o in obstacles} | {o.ur.x for o in obstacles}
)
ys = sorted(
{region.ll.y, region.ur.y} | {o.ll.y for o in obstacles} | {o.ur.y for o in obstacles}
)
rooms: list[IntBox] = []
for j in range(len(ys) - 1):
y0, y1 = ys[j], ys[j + 1]
if y1 <= y0:
continue
run: IntBox | None = None
for i in range(len(xs) - 1):
x0, x1 = xs[i], xs[i + 1]
if x1 <= x0:
continue
cx, cy = (x0 + x1) // 2, (y0 + y1) // 2
blocked = any(o.contains_inside(IntPoint(cx, cy)) for o in obstacles)
if blocked:
if run is not None:
rooms.append(run)
run = None
continue
cell = IntBox(x0, y0, x1, y1)
run = cell if run is None else run.union(cell) # merge along x
if run is not None:
rooms.append(run)
return rooms
# --- doors -------------------------------------------------------------------
def _build_graph(rooms_by_layer: dict[int, list[IntBox]], trace_width: int) -> _Graph:
graph = _Graph()
next_id = 0
layer_rooms: dict[int, list[_Room]] = {}
for layer, boxes in rooms_by_layer.items():
layer_rooms[layer] = []
for box in boxes:
room = _Room(next_id, layer, box)
graph.rooms[room.id] = room
graph.doors[room.id] = []
graph.by_layer.setdefault(layer, []).append(room.id)
layer_rooms[layer].append(room)
next_id += 1
# same-layer edge doors (shared edge with overlap >= trace_width)
for rooms in layer_rooms.values():
for i in range(len(rooms)):
a = rooms[i]
for j in range(i + 1, len(rooms)):
b = rooms[j]
door = _edge_door(a.box, b.box, trace_width)
if door is not None:
lo, hi = door
graph.doors[a.id].append(_Door(b.id, "edge", lo, hi))
graph.doors[b.id].append(_Door(a.id, "edge", lo, hi))
# via doors: overlapping rooms on different layers
layers = sorted(layer_rooms)
for li in range(len(layers)):
for lj in range(li + 1, len(layers)):
for a in layer_rooms[layers[li]]:
for b in layer_rooms[layers[lj]]:
inter = a.box.intersection(b.box)
if not inter.is_empty() and inter.min_width() >= trace_width:
lo = IntPoint(inter.ll.x, inter.ll.y)
hi = IntPoint(inter.ur.x, inter.ur.y)
graph.doors[a.id].append(_Door(b.id, "via", lo, hi))
graph.doors[b.id].append(_Door(a.id, "via", lo, hi))
return graph
def _edge_door(a: IntBox, b: IntBox, trace_width: int):
"""Shared edge segment of two boxes if they abut with enough overlap."""
if a.ur.x == b.ll.x or b.ur.x == a.ll.x: # vertical shared edge
x = a.ur.x if a.ur.x == b.ll.x else a.ll.x
lo_y = max(a.ll.y, b.ll.y)
hi_y = min(a.ur.y, b.ur.y)
if hi_y - lo_y >= trace_width:
return IntPoint(x, lo_y), IntPoint(x, hi_y)
if a.ur.y == b.ll.y or b.ur.y == a.ll.y: # horizontal shared edge
y = a.ur.y if a.ur.y == b.ll.y else a.ll.y
lo_x = max(a.ll.x, b.ll.x)
hi_x = min(a.ur.x, b.ur.x)
if hi_x - lo_x >= trace_width:
return IntPoint(lo_x, y), IntPoint(hi_x, y)
return None
# --- room A* -----------------------------------------------------------------
def _rooms_containing(graph: _Graph, point: IntPoint, layers) -> list[int]:
return [
rid
for layer in layers
for rid in graph.by_layer.get(layer, [])
if graph.rooms[rid].box.contains(point)
]
def _search_rooms(graph: _Graph, starts, goals, goal_pt, via_cost):
goal_set = set(goals)
if not goal_set:
return None
open_heap: list[tuple[float, int]] = []
g_score: dict[int, float] = {}
came: dict[int, tuple[int, _Door]] = {}
for s in sorted(starts):
g_score[s] = 0.0
heapq.heappush(open_heap, (_dist(graph.rooms[s].center(), goal_pt), s))
while open_heap:
_, cur = heapq.heappop(open_heap)
if cur in goal_set:
return _reconstruct_rooms(came, cur)
base = g_score[cur]
c_center = graph.rooms[cur].center()
for door in graph.doors[cur]:
step = _dist(c_center, graph.rooms[door.other].center())
if door.kind == "via":
step += via_cost
tentative = base + step
if tentative < g_score.get(door.other, float("inf")):
came[door.other] = (cur, door)
g_score[door.other] = tentative
h = _dist(graph.rooms[door.other].center(), goal_pt)
heapq.heappush(open_heap, (tentative + h, door.other))
return None
def _reconstruct_rooms(came, cur):
seq = [(cur, None)]
while cur in came:
prev, door = came[cur]
seq.append((prev, door))
cur = prev
seq.reverse()
return seq # list of (room_id, door_used_to_enter_or_None_for_start)
def _dist(a: IntPoint, b: IntPoint) -> float:
return abs(a.x - b.x) + abs(a.y - b.y)
# --- path realization --------------------------------------------------------
def _realize(graph, seq, start: IntPoint, goal: IntPoint, half_width, occupancy=None):
"""Turn a room sequence into per-layer orthogonal wire segments + vias.
Routes through each door's gate with an L-connector, splitting the path at
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]]] = []
vias: list[IntPoint] = []
layer = graph.rooms[seq[0][0]].layer
pts = [start]
# seq[i] carries the door from room i to room i+1 (seq[-1] has None)
for i in range(len(seq) - 1):
door = seq[i][1]
if door is None:
return None
if door.kind == "edge":
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)
else: # via: close the current-layer wire, drop a via, start next layer
gate = _via_gate(door, pts[-1], half_width)
pts.append(gate)
wires.append((layer, list(pts)))
vias.append(gate)
layer = graph.rooms[door.other].layer
pts = [gate]
pts.append(goal)
wires.append((layer, pts))
# expand each polyline of gate points into orthogonal L-connected corners
ortho: list[tuple[int, list[IntPoint]]] = []
for lyr, poly in wires:
corners = _orthogonalize(poly)
if len(corners) >= 2:
ortho.append((lyr, corners))
return ortho, vias
def _edge_gate(door: _Door, from_pt: IntPoint, half_width: int) -> IntPoint:
"""A point on the door edge, kept >= half_width from the door ends."""
if door.lo.x == door.hi.x: # vertical edge -> pick y
y = _clamp(from_pt.y, door.lo.y + half_width, door.hi.y - half_width)
return IntPoint(door.lo.x, y)
x = _clamp(from_pt.x, door.lo.x + half_width, door.hi.x - half_width)
return IntPoint(x, door.lo.y)
def _via_gate(door: _Door, from_pt: IntPoint, half_width: int) -> IntPoint:
x = _clamp(from_pt.x, door.lo.x + half_width, door.hi.x - half_width)
y = _clamp(from_pt.y, door.lo.y + half_width, door.hi.y - half_width)
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]:
"""Connect gate points with axis-aligned segments (L-shaped between each)."""
out = [points[0]]
for p in points[1:]:
last = out[-1]
if last.x != p.x and last.y != p.y:
out.append(IntPoint(p.x, last.y)) # horizontal then vertical
if not (out[-1].x == p.x and out[-1].y == p.y):
out.append(p)
return _dedupe(out)
def _dedupe(points: list[IntPoint]) -> list[IntPoint]:
out: list[IntPoint] = []
for p in points:
if out and out[-1].x == p.x and out[-1].y == p.y:
continue
if len(out) >= 2:
a, b = out[-2], out[-1]
if (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) == 0:
out[-1] = p
continue
out.append(p)
return out
def _clamp(v: int, lo: int, hi: int) -> int:
if lo > hi:
return (lo + hi) // 2
return max(lo, min(hi, v))
# --- orchestration -----------------------------------------------------------
def route_board_rooms(
board: BasicBoard,
*,
trace_width: int,
clearance: int,
layers: list[int] | None = None,
via_cost: float = 50000.0,
shove: bool = False,
max_shove_depth: int = 5,
shove_cap: int = 3,
) -> ExactRouteResult:
"""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 [
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
]
half_width = max(trace_width // 2, 1)
margin = clearance + half_width
outline = board.bounding_box
tree = ShapeSearchTree()
owner = _counter()
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:
b = tile.bounding_box()
if not b.is_empty():
tree.insert(TreeShape(next(owner), -1, frozenset({obstacle.layer}), b))
result = RouteResult(half_width)
via_layers = frozenset(signal_layers)
accepted: dict[int, _Accepted] = {}
shoves = 0
for net_no in sorted(_routable_nets(board, signal_layers)):
pins = _net_pins(board, net_no, signal_layers)
for a, b in zip(pins, pins[1:], strict=False):
shoves += _route_connection(
net_no,
a,
b,
tree,
result,
owner,
outline,
signal_layers,
half_width,
clearance,
margin,
trace_width,
via_cost,
via_layers,
shove,
accepted,
max_shove_depth,
shove_cap,
)
drc_clean = tree.has_violation(clearance) is None
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean, shoves=shoves)
def _route_connection(
net_no,
a,
b,
tree,
result,
owner,
outline,
signal_layers,
half_width,
clearance,
margin,
trace_width,
via_cost,
via_layers,
shove=False,
accepted=None,
max_shove_depth=5,
shove_cap=3,
):
rooms_by_layer = {
layer: _free_rooms(tree, outline, layer, net_no, margin, half_width)
for layer in signal_layers
}
graph = _build_graph(rooms_by_layer, trace_width)
a_layers = [layer for layer in a.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)
goals = _rooms_containing(graph, b.location, b_layers)
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]]] = []
for layer, corners in wires:
for box in PolylineShape(Polyline(corners), half_width).tiles():
if not outline.contains_box(box):
return None
boxes.append((box, frozenset({layer})))
for loc in vias:
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 _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
acc = accepted.get(net_no)
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:
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)
acc.wire_owners.append(w_owners)
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)
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]:
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 _counter():
i = 0
while True:
yield i
i += 1