Tracks and vias already on the board were ignored, so other nets could be routed straight through them (the flight controller's 82 GND stitching vias and its guard tracks). They are now copper of their net: other nets keep a pad's distance from them in the grid, the exact check and the room engine, and they add no connections to route.
1221 lines
45 KiB
Python
1221 lines
45 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,
|
|
pack: bool = False,
|
|
max_shove_depth: int = 5,
|
|
shove_cap: int = 3,
|
|
via_half: int | None = None,
|
|
edge_clearance: int | None = None,
|
|
) -> 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.
|
|
|
|
``pack`` (opt-in, independent of ``shove``) runs the coordinated channel
|
|
packer first: groups of 2-pin nets that must cross a common obstacle gap are
|
|
assigned consistent parallel lanes across the gap and routed together, so a
|
|
channel that is physically wide enough for all of them does not drop nets to
|
|
greedy per-net gate collisions. With ``pack=False`` the pre-pass is skipped
|
|
and the output is unchanged.
|
|
"""
|
|
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)
|
|
# half the via pad's size: a via is not a trace, and sizing it like one
|
|
# let routes pass within a via's real copper
|
|
via_half = via_half or half_width
|
|
edge = (board.outline, 0 if edge_clearance is None else edge_clearance)
|
|
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 net, copper_layers, box in board.get_fixed_copper():
|
|
on = frozenset(layer for layer in copper_layers if layer in signal_layers)
|
|
if on and not box.is_empty():
|
|
tree.insert(TreeShape(next(owner), net, on, 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
|
|
packed: set[int] = set()
|
|
if pack:
|
|
packed = _pack_channels(
|
|
board,
|
|
tree,
|
|
result,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
trace_width,
|
|
via_layers,
|
|
accepted,
|
|
shove,
|
|
via_half=via_half, edge=edge)
|
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
|
if net_no in packed:
|
|
continue
|
|
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,
|
|
via_half=via_half, edge=edge)
|
|
|
|
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,
|
|
via_half=None,
|
|
edge=None,
|
|
):
|
|
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, via_half=via_half, edge=edge)
|
|
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
|
|
, via_half=via_half)
|
|
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,
|
|
via_half=via_half, edge=edge)
|
|
return 0
|
|
|
|
|
|
def _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=None, edge=None):
|
|
via_half = via_half or half_width
|
|
"""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
|
|
if edge is not None and edge[0] is not None and not edge[0].box_clear(box, edge[1]):
|
|
return None
|
|
boxes.append((box, frozenset({layer})))
|
|
for loc in vias:
|
|
vbox = IntBox(
|
|
loc.x - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half
|
|
)
|
|
boxes.append((vbox, via_layers))
|
|
return boxes
|
|
|
|
|
|
def _commit(
|
|
net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted, via_half=None
|
|
):
|
|
via_half = via_half or half_width
|
|
"""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 - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half
|
|
)
|
|
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 - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half
|
|
)
|
|
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)
|
|
|
|
|
|
# --- coordinated channel packing ---------------------------------------------
|
|
|
|
|
|
def _long(p: IntPoint, axis: str) -> int:
|
|
"""Coordinate along a channel's length (the axis nets travel)."""
|
|
return p.y if axis == "v" else p.x
|
|
|
|
|
|
def _trans(p: IntPoint, axis: str) -> int:
|
|
"""Coordinate across a channel (the axis lanes spread over)."""
|
|
return p.x if axis == "v" else p.y
|
|
|
|
|
|
def _box_long(b: IntBox, axis: str) -> tuple[int, int]:
|
|
return (b.ll.y, b.ur.y) if axis == "v" else (b.ll.x, b.ur.x)
|
|
|
|
|
|
def _box_trans(b: IntBox, axis: str) -> tuple[int, int]:
|
|
return (b.ll.x, b.ur.x) if axis == "v" else (b.ll.y, b.ur.y)
|
|
|
|
|
|
def _outline_trans(outline: IntBox, axis: str) -> tuple[int, int]:
|
|
return (outline.min_x, outline.max_x) if axis == "v" else (outline.min_y, outline.max_y)
|
|
|
|
|
|
def _axis_point(long_val: int, trans_val: int, axis: str) -> IntPoint:
|
|
"""Build a point from (length-coord, transverse-coord) for the given axis."""
|
|
return IntPoint(trans_val, long_val) if axis == "v" else IntPoint(long_val, trans_val)
|
|
|
|
|
|
def _lane_polyline(
|
|
top: IntPoint, bot: IntPoint, lane: int, trunk_top: int, trunk_bot: int, axis: str
|
|
) -> list[IntPoint]:
|
|
"""A pad -> trunk -> lane -> trunk -> pad orthogonal trace.
|
|
|
|
Each net converges to its lane on a *staggered* trunk line (``trunk_top`` /
|
|
``trunk_bot``, a distinct length-coordinate per net just outside the gap), runs
|
|
straight through the channel in its lane, then diverges to the far pad on the
|
|
other trunk. Distinct trunk lines keep the fan-in horizontals from overlapping
|
|
when pads spread wider than the gap. ``top`` is the pin with the larger
|
|
length-coordinate. Collinear/duplicate points collapse."""
|
|
tt = _trans(top, axis)
|
|
tb = _trans(bot, axis)
|
|
pts = [
|
|
top,
|
|
_axis_point(trunk_top, tt, axis),
|
|
_axis_point(trunk_top, lane, axis),
|
|
_axis_point(trunk_bot, lane, axis),
|
|
_axis_point(trunk_bot, tb, axis),
|
|
bot,
|
|
]
|
|
return _dedupe(pts)
|
|
|
|
|
|
def _wall_rows(obstacles: list[IntBox], axis: str) -> list[list[IntBox]]:
|
|
"""Cluster obstacle boxes into walls whose blocked length-ranges overlap; each
|
|
cluster is one barrier that nets cross transversely through its gaps."""
|
|
boxes = sorted(obstacles, key=lambda b: (_box_long(b, axis), _box_trans(b, axis)))
|
|
used = [False] * len(boxes)
|
|
rows: list[list[IntBox]] = []
|
|
for i, b in enumerate(boxes):
|
|
if used[i]:
|
|
continue
|
|
used[i] = True
|
|
cluster = [b]
|
|
lo, hi = _box_long(b, axis)
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for j, c in enumerate(boxes):
|
|
if used[j]:
|
|
continue
|
|
clo, chi = _box_long(c, axis)
|
|
if clo < hi and chi > lo:
|
|
used[j] = True
|
|
cluster.append(c)
|
|
lo, hi = min(lo, clo), max(hi, chi)
|
|
changed = True
|
|
rows.append(cluster)
|
|
return rows
|
|
|
|
|
|
def _channel_groups(obstacles, cands, axis, trace_width):
|
|
"""Find (gap_lo, gap_hi, [net_no, ...]) channels on one axis: a gap between two
|
|
obstacles in a wall row, and the >= 2 candidate nets that must cross that wall
|
|
and are nearest this gap. Only gaps bounded by an obstacle on both sides count
|
|
(an open board edge is not a channel)."""
|
|
groups = []
|
|
for cluster in _wall_rows(obstacles, axis):
|
|
blo = max(_box_long(b, axis)[0] for b in cluster)
|
|
bhi = min(_box_long(b, axis)[1] for b in cluster)
|
|
if bhi <= blo:
|
|
continue # no length-band the whole cluster blocks in common
|
|
occ = sorted(_box_trans(b, axis) for b in cluster)
|
|
merged: list[tuple[int, int]] = []
|
|
for lo, hi in occ:
|
|
if merged and lo <= merged[-1][1]:
|
|
merged[-1] = (merged[-1][0], max(merged[-1][1], hi))
|
|
else:
|
|
merged.append((lo, hi))
|
|
gaps = [
|
|
(merged[k][1], merged[k + 1][0])
|
|
for k in range(len(merged) - 1)
|
|
if merged[k + 1][0] - merged[k][1] >= trace_width
|
|
]
|
|
if not gaps:
|
|
continue
|
|
crossing = []
|
|
for net_no, (pa, pb) in cands.items():
|
|
la, lb = _long(pa, axis), _long(pb, axis)
|
|
if (la <= blo and lb >= bhi) or (lb <= blo and la >= bhi):
|
|
crossing.append(net_no)
|
|
for gap_lo, gap_hi in gaps:
|
|
members = []
|
|
for net_no in crossing:
|
|
pa, pb = cands[net_no]
|
|
center = (_trans(pa, axis) + _trans(pb, axis)) / 2
|
|
nearest = min(gaps, key=lambda g: abs((g[0] + g[1]) / 2 - center))
|
|
if nearest == (gap_lo, gap_hi):
|
|
members.append(net_no)
|
|
if len(members) >= 2:
|
|
groups.append((gap_lo, gap_hi, blo, bhi, sorted(members)))
|
|
return groups
|
|
|
|
|
|
def _boxes_clear(boxes, net_no, placed, clearance) -> bool:
|
|
"""True if none of ``boxes`` come within ``clearance`` of a different-net box in
|
|
``placed`` (list of ``(net_no, layer, box)``) -- the in-group lane-vs-lane check
|
|
that the search tree cannot do until copper is committed."""
|
|
for box, box_layers in boxes:
|
|
expanded = box.offset(clearance)
|
|
for other_net, other_layer, other_box in placed:
|
|
if other_net == net_no or other_layer not in box_layers:
|
|
continue
|
|
if other_box.overlaps(expanded):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _pack_channels(
|
|
board,
|
|
tree,
|
|
result,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
trace_width,
|
|
via_layers,
|
|
accepted,
|
|
shove,
|
|
via_half=None,
|
|
edge=None,
|
|
) -> set[int]:
|
|
"""Coordinated multi-trace channel packing (opt-in pre-pass).
|
|
|
|
Greedy per-net routing sends each net through a shared constriction on its own
|
|
independently chosen path; the paths collide and some nets drop even when the
|
|
gap is physically wide enough for all of them. This pass groups 2-pin nets that
|
|
must cross a common obstacle gap, assigns each a parallel lane across the gap's
|
|
usable width -- sorted by pad position, spacing >= trace_width + clearance so
|
|
lanes never conflict and, being pad-ordered, never cross -- and realizes each as
|
|
an orthogonal converge/straight/diverge trace verified exactly DRC-clean against
|
|
the search tree. A net whose lane trace is not clean (e.g. it clips a pad) is
|
|
left for the normal loop. Returns the fully-routed net numbers so the caller
|
|
skips them. Scope: single-layer 2-pin nets through one axis-aligned gap."""
|
|
routed: set[int] = set()
|
|
spacing_min = trace_width + clearance
|
|
cands: dict[int, tuple[int, IntPoint, IntPoint]] = {}
|
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
|
pins = _net_pins(board, net_no, signal_layers)
|
|
if len(pins) != 2:
|
|
continue
|
|
shared = [
|
|
layer for layer in pins[0].layers if layer in pins[1].layers and layer in signal_layers
|
|
]
|
|
if not shared:
|
|
continue
|
|
cands[net_no] = (min(shared), pins[0].location, pins[1].location)
|
|
if len(cands) < 2:
|
|
return routed
|
|
|
|
obstacles_by_layer: dict[int, list[IntBox]] = {layer: [] for layer in signal_layers}
|
|
for obstacle in board.get_obstacle_areas():
|
|
if obstacle.layer not in signal_layers:
|
|
continue
|
|
for tile in obstacle.tiles:
|
|
box = tile.bounding_box()
|
|
if not box.is_empty():
|
|
obstacles_by_layer[obstacle.layer].append(box)
|
|
|
|
for layer in sorted(signal_layers):
|
|
obstacles = obstacles_by_layer.get(layer, [])
|
|
if not obstacles:
|
|
continue
|
|
layer_cands = {net_no: (pa, pb) for net_no, (lyr, pa, pb) in cands.items() if lyr == layer}
|
|
if len(layer_cands) < 2:
|
|
continue
|
|
for axis in ("v", "h"):
|
|
for gap_lo, gap_hi, blo, bhi, members in _channel_groups(
|
|
obstacles, layer_cands, axis, trace_width
|
|
):
|
|
members = [n for n in members if n not in routed]
|
|
if len(members) < 2:
|
|
continue
|
|
placed = _pack_group(
|
|
members,
|
|
layer_cands,
|
|
axis,
|
|
layer,
|
|
gap_lo,
|
|
gap_hi,
|
|
blo,
|
|
bhi,
|
|
tree,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
spacing_min,
|
|
via_layers,
|
|
outline,
|
|
via_half=via_half, edge=edge)
|
|
if placed is None:
|
|
continue # could not pack every member cleanly -> leave to greedy
|
|
for net_no, wires in placed:
|
|
_commit(
|
|
net_no,
|
|
wires,
|
|
[],
|
|
tree,
|
|
result,
|
|
owner,
|
|
half_width,
|
|
via_layers,
|
|
shove,
|
|
accepted,
|
|
via_half=via_half)
|
|
routed.add(net_no)
|
|
return routed
|
|
|
|
|
|
def _pack_group(
|
|
members,
|
|
cands,
|
|
axis,
|
|
layer,
|
|
gap_lo,
|
|
gap_hi,
|
|
blo,
|
|
bhi,
|
|
tree,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
spacing_min,
|
|
via_layers,
|
|
outline,
|
|
via_half=None,
|
|
edge=None,
|
|
):
|
|
"""Assign parallel lanes + staggered fan-in trunks to every net crossing one
|
|
gap, and return ``[(net_no, wires), ...]`` if ALL of them realize exactly
|
|
DRC-clean, else ``None``. All-or-nothing: a partial pack could leave the
|
|
channel worse than greedy, so the group is committed only if it packs whole."""
|
|
count = len(members)
|
|
lo = gap_lo + margin
|
|
hi = gap_hi - margin
|
|
band = hi - lo
|
|
if band < (count - 1) * spacing_min:
|
|
return None # gap not physically wide enough for this many lanes
|
|
|
|
# per net: (top pin, bottom pin) by length-coordinate; top has the larger value
|
|
ends = {}
|
|
for net_no in members:
|
|
pa, pb = cands[net_no]
|
|
ends[net_no] = (pa, pb) if _long(pa, axis) >= _long(pb, axis) else (pb, pa)
|
|
|
|
# lanes in gap order, preserving pad order so lanes never cross
|
|
lane_order = sorted(members, key=lambda n: (_trans(ends[n][0], axis), n))
|
|
lane_of = {
|
|
net_no: (lo if count == 1 else lo + round(band * i / (count - 1)))
|
|
for i, net_no in enumerate(lane_order)
|
|
}
|
|
|
|
# staggered trunks: the widest fan-in horizontal sits nearest the gap mouth so
|
|
# it passes under the shorter nets' descents (a monotone, crossing-free fan)
|
|
top_rank = {
|
|
n: r
|
|
for r, n in enumerate(
|
|
sorted(members, key=lambda n: (-abs(_trans(ends[n][0], axis) - lane_of[n]), n))
|
|
)
|
|
}
|
|
bot_rank = {
|
|
n: r
|
|
for r, n in enumerate(
|
|
sorted(members, key=lambda n: (-abs(_trans(ends[n][1], axis) - lane_of[n]), n))
|
|
)
|
|
}
|
|
top_pad = min(_long(ends[n][0], axis) for n in members)
|
|
bot_pad = max(_long(ends[n][1], axis) for n in members)
|
|
if bhi + margin + (count - 1) * spacing_min > top_pad - margin:
|
|
return None # not enough room above the gap to stack the fan-in trunks
|
|
if blo - margin - (count - 1) * spacing_min < bot_pad + margin:
|
|
return None # not enough room below the gap
|
|
|
|
placed_boxes: list[tuple[int, int, IntBox]] = []
|
|
to_commit = []
|
|
for net_no in sorted(members):
|
|
top_pt, bot_pt = ends[net_no]
|
|
trunk_top = bhi + margin + top_rank[net_no] * spacing_min
|
|
trunk_bot = blo - margin - bot_rank[net_no] * spacing_min
|
|
corners = _lane_polyline(top_pt, bot_pt, lane_of[net_no], trunk_top, trunk_bot, axis)
|
|
if len(corners) < 2:
|
|
return None
|
|
wires = [(layer, corners)]
|
|
boxes = _collect_boxes(wires, [], half_width, via_layers, outline, via_half=via_half, edge=edge)
|
|
if boxes is None:
|
|
return None
|
|
if any(
|
|
tree.clearance_conflict(box, net_no, box_layer, clearance)
|
|
for box, box_layers in boxes
|
|
for box_layer in box_layers
|
|
):
|
|
return None
|
|
if not _boxes_clear(boxes, net_no, placed_boxes, clearance):
|
|
return None
|
|
for box, box_layers in boxes:
|
|
for box_layer in box_layers:
|
|
placed_boxes.append((net_no, box_layer, box))
|
|
to_commit.append((net_no, wires))
|
|
return to_commit
|
|
|
|
|
|
# --- 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,
|
|
via_half=None,
|
|
edge=None,
|
|
) -> 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)
|
|
# Reuse this net's existing registry if it already committed connections, so
|
|
# ``wire_owners`` stays parallel to ``wires``. Clobbering with a fresh
|
|
# ``_Accepted`` here would leave ``wire_owners`` shorter than ``wires`` (its
|
|
# ``result.wires[net_no]`` list already holds the earlier connections),
|
|
# desyncing the two lists and making a later shove remove the wrong wire's
|
|
# copper from the tree while it stays in the emitted route.
|
|
acc = accepted.get(net_no) if accepted is not None else None
|
|
if acc is None:
|
|
acc = _Accepted(
|
|
wires=result.wires.setdefault(net_no, []), vias=result.vias.setdefault(net_no, [])
|
|
)
|
|
accepted[net_no] = acc
|
|
result.add_wire(net_no, layer, corners)
|
|
acc.wire_owners.append(owners)
|
|
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
|