Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783dd8f0d1 | ||
|
|
8a82143517 | ||
|
|
8a50bde531 | ||
|
|
0f6a2a0812 | ||
|
|
245cd91c0e | ||
|
|
019b043cd5 | ||
|
|
a2e108bc51 | ||
|
|
3462089e09 |
@@ -70,6 +70,27 @@ class BasicBoard:
|
||||
def get_traces(self) -> list[Trace]:
|
||||
return [it for it in self._items if isinstance(it, Trace)]
|
||||
|
||||
def get_fixed_copper(self) -> list[tuple[int, frozenset[int], IntBox]]:
|
||||
"""Copper already routed on the board (the DSN's ``wiring``), as
|
||||
``(net, layers, box)``. A track is its segments, its corners and the
|
||||
round caps at its ends (as squares); a via is its largest pad."""
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape, corner_box
|
||||
|
||||
out: list[tuple[int, frozenset[int], IntBox]] = []
|
||||
for item in self._items:
|
||||
if not item.fixed or not item.net_nos:
|
||||
continue
|
||||
net = item.net_nos[0]
|
||||
if isinstance(item, Trace) and not item.is_empty():
|
||||
layers = frozenset({item.layer})
|
||||
for box in PolylineShape(Polyline(item.corners), item.half_width).tiles():
|
||||
out.append((net, layers, box))
|
||||
for end in (item.corners[0], item.corners[-1]):
|
||||
out.append((net, layers, corner_box(end, item.half_width)))
|
||||
elif isinstance(item, Via) and item.shape is not None:
|
||||
out.append((net, frozenset(item.layers), item.shape.bounding_box()))
|
||||
return out
|
||||
|
||||
def get_vias(self) -> list[Via]:
|
||||
return [it for it in self._items if isinstance(it, Via)]
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from freeroute.geometry.polygon_shape import PolygonShape
|
||||
|
||||
from .board import BasicBoard
|
||||
from .clearance import ClearanceMatrix
|
||||
from .items import ConductionArea, ObstacleArea, Pin
|
||||
from .items import ConductionArea, ObstacleArea, Pin, Trace, Via
|
||||
from .layer import Layer, LayerStructure
|
||||
from .net import Nets
|
||||
from .transform import CoordinateTransform
|
||||
@@ -56,6 +56,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
||||
_insert_keepouts(dsn, board, transform)
|
||||
_insert_image_keepouts(dsn, board, transform)
|
||||
_insert_zones(dsn, board, transform, nets)
|
||||
_insert_wiring(dsn, board, transform, nets)
|
||||
return board
|
||||
|
||||
|
||||
@@ -471,6 +472,47 @@ def _placed_area_tiles(shape: Shape, place, transform: CoordinateTransform) -> l
|
||||
return [tile for tile in PolygonShape(pts).split_to_convex() if not tile.is_empty()]
|
||||
|
||||
|
||||
def _insert_wiring(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
||||
"""Tracks and vias already on the board, as fixed copper of their nets."""
|
||||
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
||||
numbers = {net.name: net.net_number for net in nets}
|
||||
for wire in dsn.wires:
|
||||
if wire.net not in numbers or wire.layer not in layer_no:
|
||||
continue
|
||||
c = wire.coords
|
||||
corners = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
||||
if len(corners) < 2:
|
||||
continue
|
||||
board.insert(
|
||||
Trace(
|
||||
net_nos=[numbers[wire.net]], fixed=True, layer=layer_no[wire.layer],
|
||||
half_width=max(round(transform.dsn_to_board(wire.width / 2)), 1), corners=corners,
|
||||
)
|
||||
)
|
||||
for via in dsn.vias:
|
||||
padstack = dsn.padstack(via.padstack)
|
||||
if via.net not in numbers or padstack is None or not padstack.shapes:
|
||||
continue
|
||||
layers: set[int] = set()
|
||||
for shape in padstack.shapes:
|
||||
if shape.layer in ("signal", "pcb"):
|
||||
layers.update(signal_layers)
|
||||
elif shape.layer in layer_no:
|
||||
layers.add(layer_no[shape.layer])
|
||||
location = transform.dsn_to_board_point(via.x, via.y)
|
||||
pad = _pad_tile(max(padstack.shapes, key=_shape_extent), transform)
|
||||
box = pad.bounding_box()
|
||||
board.insert(
|
||||
Via(
|
||||
net_nos=[numbers[via.net]], fixed=True, padstack_name=via.padstack,
|
||||
location=location, layers=sorted(layers or signal_layers),
|
||||
shape=IntBox(box.ll.x + location.x, box.ll.y + location.y,
|
||||
box.ur.x + location.x, box.ur.y + location.y),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _insert_zones(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
||||
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
||||
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||
|
||||
@@ -114,6 +114,9 @@ class Via(Item):
|
||||
|
||||
padstack_name: str = ""
|
||||
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
|
||||
#: layer indices the via's pads occupy, and the largest pad as a box
|
||||
layers: list[int] = field(default_factory=list)
|
||||
shape: object = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+12
-10
@@ -30,8 +30,8 @@ from freeroute.ses import write_ses
|
||||
#: which optional flags each engine actually implements. Anything not listed for
|
||||
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
||||
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
||||
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote", "access", "escapes"}),
|
||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access", "escapes"}),
|
||||
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote", "access", "rollback"}),
|
||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access", "rollback"}),
|
||||
"room": frozenset({"shove", "pack"}),
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ _FLAG_NAMES = {
|
||||
"orders": "--orders",
|
||||
"promote": "--promote-failures",
|
||||
"access": "--access-cost",
|
||||
"escapes": "--reserve-escapes",
|
||||
"rollback": "--no-rollback",
|
||||
}
|
||||
|
||||
|
||||
@@ -168,10 +168,12 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"another pin's way out while that pin still has connections to route (default 0: off)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reserve-escapes",
|
||||
action="store_true",
|
||||
help="grid and exact engines: before routing, reserve a way out for every fine-pitch "
|
||||
"pin that no other net may use until that pin's connections are routed",
|
||||
"--no-rollback",
|
||||
dest="rollback",
|
||||
action="store_false",
|
||||
help="grid and exact engines: keep every rip-up, as before. By default the "
|
||||
"connections a rip-up displaces are rerouted at once and the rip-up is undone "
|
||||
"if fewer connections end up routed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jobs",
|
||||
@@ -202,7 +204,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
("orders", args.orders is not None),
|
||||
("promote", args.promote_failures is not None),
|
||||
("access", args.access_cost is not None),
|
||||
("escapes", args.reserve_escapes),
|
||||
("rollback", not args.rollback),
|
||||
)
|
||||
if asked
|
||||
]
|
||||
@@ -237,7 +239,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"resolution": 2 if args.grid_resolution is None else args.grid_resolution,
|
||||
"heuristic_weight": args.heuristic_weight,
|
||||
"access_cost": args.access_cost or 0.0,
|
||||
"reserve_escapes": args.reserve_escapes,
|
||||
"rollback": args.rollback,
|
||||
}
|
||||
elif args.engine == "grid":
|
||||
options = {
|
||||
@@ -247,7 +249,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"edge_clearance_mm": args.edge_clearance,
|
||||
"resolution": 1 if args.grid_resolution is None else args.grid_resolution,
|
||||
"access_cost": args.access_cost or 0.0,
|
||||
"reserve_escapes": args.reserve_escapes,
|
||||
"rollback": args.rollback,
|
||||
}
|
||||
if args.engine == "room":
|
||||
result = build_rooms_routing_result(
|
||||
|
||||
@@ -27,6 +27,8 @@ __all__ = [
|
||||
"NetPin",
|
||||
"Net",
|
||||
"NetClass",
|
||||
"WiringVia",
|
||||
"WiringWire",
|
||||
"Keepout",
|
||||
"DsnBoard",
|
||||
]
|
||||
@@ -186,6 +188,26 @@ class Keepout:
|
||||
kind: str = "keepout" # keepout | via_keepout | wire_keepout | place_keepout
|
||||
|
||||
|
||||
@dataclass
|
||||
class WiringWire:
|
||||
"""A track already on the board: ``(wire (path LAYER WIDTH x y ...) (net N))``."""
|
||||
|
||||
net: str
|
||||
layer: str
|
||||
width: float
|
||||
coords: list[float] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WiringVia:
|
||||
"""A via already on the board: ``(via PADSTACK x y (net N))``."""
|
||||
|
||||
net: str
|
||||
padstack: str
|
||||
x: float
|
||||
y: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class DsnBoard:
|
||||
"""The fully parsed DSN file."""
|
||||
@@ -197,6 +219,9 @@ class DsnBoard:
|
||||
layers: list[Layer] = field(default_factory=list)
|
||||
#: copper zones, as planes of a net
|
||||
planes: list[Plane] = field(default_factory=list)
|
||||
#: copper already routed (the ``wiring`` scope), kept fixed
|
||||
wires: list[WiringWire] = field(default_factory=list)
|
||||
vias: list[WiringVia] = field(default_factory=list)
|
||||
#: bounding-box boundary shape (layer ``pcb``), if present
|
||||
boundary: Shape | None = None
|
||||
#: outline shapes (layer ``signal``)
|
||||
|
||||
@@ -27,6 +27,8 @@ from .model import (
|
||||
Pin,
|
||||
Resolution,
|
||||
WidthRule,
|
||||
WiringVia,
|
||||
WiringWire,
|
||||
)
|
||||
from .sexp import SExp, parse
|
||||
from .shapes import Shape, read_area, read_shape
|
||||
@@ -531,11 +533,45 @@ def parse_dsn(text: str) -> DsnBoard:
|
||||
_read_placement(it, board)
|
||||
elif head == "network":
|
||||
_read_network(it, board)
|
||||
# `wiring` and unknown scopes are intentionally ignored for now.
|
||||
elif head == "wiring":
|
||||
_read_wiring(it, board)
|
||||
# unknown scopes are ignored
|
||||
|
||||
return board
|
||||
|
||||
|
||||
def _read_wiring(node: SExp, board: DsnBoard) -> None:
|
||||
"""Tracks and vias already on the board. They stay where they are: the
|
||||
router treats them as copper of their net that other nets keep clear of."""
|
||||
for it in node.items:
|
||||
if not isinstance(it, SExp):
|
||||
continue
|
||||
net_scope = it.child("net")
|
||||
net = _first_value(net_scope) if net_scope is not None else None
|
||||
if not net:
|
||||
continue # netless copper would need a keepout, not a wire
|
||||
if it.head == "wire":
|
||||
path = it.child("path") or it.child("polyline_path")
|
||||
vals = [v.text for v in path.values()] if path is not None else []
|
||||
if len(vals) < 6:
|
||||
continue
|
||||
try:
|
||||
width = float(vals[1])
|
||||
coords = [float(v) for v in vals[2:]]
|
||||
except ValueError:
|
||||
continue
|
||||
board.wires.append(WiringWire(net=net, layer=vals[0], width=width, coords=coords))
|
||||
elif it.head == "via":
|
||||
vals = [v.text for v in it.values()]
|
||||
if len(vals) < 3:
|
||||
continue
|
||||
try:
|
||||
x, y = float(vals[1]), float(vals[2])
|
||||
except ValueError:
|
||||
continue
|
||||
board.vias.append(WiringVia(net=net, padstack=vals[0], x=x, y=y))
|
||||
|
||||
|
||||
def _read_plane(node: SExp):
|
||||
"""``(plane <net> (polygon <layer> <width> x y ...))`` -> :class:`Plane`."""
|
||||
from .model import Plane
|
||||
|
||||
@@ -98,7 +98,7 @@ def route_board_exact(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
@@ -130,7 +130,7 @@ def route_board_exact(
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
reserve_escapes=reserve_escapes,
|
||||
rollback=rollback,
|
||||
)
|
||||
edge = 0 if edge_clearance is None else edge_clearance
|
||||
outline = board.outline
|
||||
@@ -149,6 +149,10 @@ def route_board_exact(
|
||||
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
|
||||
|
||||
@@ -113,6 +113,7 @@ class _Connection:
|
||||
"b_group",
|
||||
"a_end",
|
||||
"b_end",
|
||||
"path",
|
||||
)
|
||||
|
||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
||||
@@ -134,6 +135,8 @@ class _Connection:
|
||||
self.b_group: tuple[list[Pin], list] = ([b], [])
|
||||
self.a_end: Pin | None = a
|
||||
self.b_end: Pin | None = b
|
||||
# the node path last committed, so a rip-up can be undone
|
||||
self.path: list[Node] | None = None
|
||||
|
||||
|
||||
class GridRouter:
|
||||
@@ -159,7 +162,7 @@ class GridRouter:
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> None:
|
||||
self.board = board
|
||||
# Pin access: while a pin still has connections to route, the cells
|
||||
@@ -169,12 +172,12 @@ class GridRouter:
|
||||
self.access_cost = max(float(access_cost), 0.0)
|
||||
self._doorways: dict[Node, list[tuple[int, int]]] = {}
|
||||
self._pending: dict[int, int] = {}
|
||||
# Escape reservations: before routing, every crowded (fine-pitch) pin
|
||||
# gets a short way out to open board, and while it still has
|
||||
# connections to route no other net may use those cells (nor come
|
||||
# within clearance of them).
|
||||
self.reserve_escapes = bool(reserve_escapes)
|
||||
self._reserved: dict[Node, list[tuple[int, int]]] = {}
|
||||
# Rip-up as a transaction: the connections a rip-up route crosses are
|
||||
# rerouted at once, and if fewer connections are routed afterwards
|
||||
# than before, everything goes back as it was. On by default: over
|
||||
# 8 net orders it left 25 to 30 fewer connections unrouted on dense
|
||||
# boards and was never clearly worse.
|
||||
self.rollback = bool(rollback)
|
||||
# None keeps the deterministic order; a seed shuffles which connection
|
||||
# routes first, to measure how much a result owes to the order
|
||||
self.seed = seed
|
||||
@@ -247,6 +250,7 @@ class GridRouter:
|
||||
# trace halos: node -> indices of the connections claiming it
|
||||
self._halo: dict[Node, list[int]] = {}
|
||||
self._rasterize_obstacles()
|
||||
self._rasterize_fixed_copper()
|
||||
|
||||
# --- grid helpers -------------------------------------------------------
|
||||
|
||||
@@ -307,6 +311,9 @@ class GridRouter:
|
||||
self._via_edge_block: set[Cell] = set()
|
||||
# wire keepouts as boxes, to check a pin stub the way the exact engine will
|
||||
self._keepout_boxes: dict[int, list[tuple[IntBox, bool]]] = {}
|
||||
# via keepouts as boxes, and other copper a via in a pad must clear
|
||||
self._via_keepout_boxes: list[tuple[IntBox, bool]] = []
|
||||
self._static_copper: list[tuple[IntBox, int]] | None = None
|
||||
# pin end nodes no wire can reach without entering a keepout
|
||||
self._via_only: set[Node] = set()
|
||||
if outline is not None:
|
||||
@@ -356,6 +363,18 @@ class GridRouter:
|
||||
self._keepout.add((*cell, item.layer))
|
||||
if item.blocks_vias:
|
||||
self._via_keepout.add((*cell, item.layer))
|
||||
if item.blocks_vias:
|
||||
self._via_keepout_boxes.append((box, item.clearance))
|
||||
|
||||
def _rasterize_fixed_copper(self) -> None:
|
||||
"""Tracks and vias already on the board: copper of their net, which
|
||||
other nets keep the same distance from as from a pad."""
|
||||
margin = self.clearance + self.half_width
|
||||
for net, layers, box in self.board.get_fixed_copper():
|
||||
on = [layer for layer in layers if layer in self.layers]
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
for layer in on:
|
||||
self._pad_block.setdefault((*cell, layer), set()).add(net)
|
||||
|
||||
# --- occupancy cost -----------------------------------------------------
|
||||
|
||||
@@ -378,10 +397,6 @@ class GridRouter:
|
||||
node = (*cell, layer)
|
||||
if wire_keepout and node in self._keepout:
|
||||
return None
|
||||
if self._reserved:
|
||||
for pin_id, owner_net in self._reserved.get(node, ()):
|
||||
if owner_net != net_no and self._pending.get(pin_id, 0) > 0:
|
||||
return None # another pin's reserved way out
|
||||
pads = self._pad_block.get(node)
|
||||
if pads and any(n != net_no for n in pads) and net_no not in self._pad_own.get(node, ()):
|
||||
return None
|
||||
@@ -442,7 +457,14 @@ class GridRouter:
|
||||
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
||||
if not starts or not goals:
|
||||
return None
|
||||
common = sorted(set(starts) & set(goals))
|
||||
# A start that is already an end joins at once, except that ending on
|
||||
# a plane there needs a via: one that cannot be placed (a via keepout,
|
||||
# another net's pad) is no end, and the search goes on from there.
|
||||
common = [
|
||||
n for n in sorted(set(starts) & set(goals))
|
||||
if not isinstance(goals[n], ConductionArea)
|
||||
or self._via_placeable((n[0], n[1]), conn.net_no, conns)
|
||||
]
|
||||
if common:
|
||||
conn.a_end, conn.b_end = starts[common[0]], goals[common[0]]
|
||||
return [common[0]]
|
||||
@@ -452,10 +474,11 @@ class GridRouter:
|
||||
g_score: dict[Node, float] = {}
|
||||
came_from: dict[Node, Node] = {}
|
||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
||||
goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if p is not None}
|
||||
# a pin end costs its stub; a zone or plane end is entered normally
|
||||
goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if isinstance(p, Pin)}
|
||||
for s in sorted(starts):
|
||||
pin = starts[s]
|
||||
g_score[s] = self._stub_cost(pin, s) if pin is not None else 0.0
|
||||
g_score[s] = self._stub_cost(pin, s) if isinstance(pin, Pin) else 0.0
|
||||
heapq.heappush(open_heap, (g_score[s] + self._h(s, goal_cell), s))
|
||||
|
||||
# Via footprints are checked lazily: a layer change goes on the heap as
|
||||
@@ -474,6 +497,12 @@ class GridRouter:
|
||||
ok = via_ok.get(cell)
|
||||
if ok is None:
|
||||
ok = via_ok[cell] = self._via_placeable(cell, net_no, conns)
|
||||
# a via leaving or reaching a walled-in pin goes in its pad
|
||||
# (see _commit), so it has to fit there exactly
|
||||
for node, ends in ((source, starts), (target, goals)):
|
||||
pin = ends.get(node)
|
||||
if ok and isinstance(pin, Pin) and self._walled(node):
|
||||
ok = self._via_fits_at(pin.location, net_no)
|
||||
if ok:
|
||||
self._relax(source, target, g_via, goal_cell, g_score, came_from, open_heap)
|
||||
continue
|
||||
@@ -537,6 +566,29 @@ class GridRouter:
|
||||
# copper too, or a stub ends up closer than clearance to a trace in the
|
||||
# cell beside it.
|
||||
|
||||
def _walled(self, node: Node) -> bool:
|
||||
"""A pin end no wire may leave or reach: only a via in its pad will do."""
|
||||
return node in self._keepout or node in self._via_only
|
||||
|
||||
def _via_fits_at(self, point: IntPoint, net_no: int) -> bool:
|
||||
"""A via centred exactly on ``point`` clears every via keepout and all
|
||||
other nets' pads and fixed copper (what a via in a pad must meet)."""
|
||||
via = IntBox(point.x - self.via_half, point.y - self.via_half,
|
||||
point.x + self.via_half, point.y + self.via_half)
|
||||
grown = via.offset(self.clearance)
|
||||
for box, needs in self._via_keepout_boxes:
|
||||
if box.overlaps(grown if needs else via):
|
||||
return False
|
||||
if self._static_copper is None:
|
||||
copper = [
|
||||
(p.shape.bounding_box(), p.net_nos[0] if p.net_nos else -1)
|
||||
for p in self.board.get_pins()
|
||||
if p.shape is not None and any(layer in self.layers for layer in p.layers)
|
||||
]
|
||||
copper += [(box, net) for net, _, box in self.board.get_fixed_copper()]
|
||||
self._static_copper = copper
|
||||
return not any(net != net_no and box.overlaps(grown) for box, net in self._static_copper)
|
||||
|
||||
def _stub_cells(self, pin_location: IntPoint, cell: Cell) -> list[Cell]:
|
||||
"""Cells where another net's trace would come too close to the stub."""
|
||||
from freeroute.board.outline import _point_segment
|
||||
@@ -799,6 +851,7 @@ class GridRouter:
|
||||
# --- commit / rip -------------------------------------------------------
|
||||
|
||||
def _commit(self, conn: _Connection, path: list[Node]) -> None:
|
||||
conn.path = list(path)
|
||||
wire_segs, via_cells = _split_path(path)
|
||||
via_cells = list(via_cells)
|
||||
if isinstance(conn.a_end, ConductionArea):
|
||||
@@ -912,6 +965,7 @@ class GridRouter:
|
||||
done: set[int] = set()
|
||||
for conn in conns:
|
||||
for pin in self._group_pins(conn):
|
||||
self._pending[id(pin)] = self._pending.get(id(pin), 0) + 1
|
||||
if id(pin) in done:
|
||||
continue
|
||||
done.add(id(pin))
|
||||
@@ -929,76 +983,6 @@ class GridRouter:
|
||||
for node in cells:
|
||||
self._doorways.setdefault(node, []).append(claim)
|
||||
|
||||
def _count_pending(self, conns: list[_Connection]) -> None:
|
||||
"""How many unrouted connections each pin is an end of."""
|
||||
for conn in conns:
|
||||
for pin in self._group_pins(conn):
|
||||
self._pending[id(pin)] = self._pending.get(id(pin), 0) + 1
|
||||
|
||||
def _reserve_escapes(self, conns: list[_Connection]) -> None:
|
||||
"""Give every crowded pin a way out before anything is routed."""
|
||||
blocked: set[Node] = set() # earlier escapes and their clearance
|
||||
spread = [(0, 0), *self._halo_offsets]
|
||||
seen: set[int] = set()
|
||||
for conn in conns:
|
||||
for pin in self._group_pins(conn):
|
||||
if id(pin) in seen or not self._crowded(pin, conn.net_no):
|
||||
continue
|
||||
seen.add(id(pin))
|
||||
path = self._escape_path(pin, conn.net_no, conns, blocked)
|
||||
if not path:
|
||||
continue
|
||||
claim = (id(pin), conn.net_no)
|
||||
for x, y, layer in path:
|
||||
for dx, dy in spread:
|
||||
node = (x + dx, y + dy, layer)
|
||||
if 0 <= node[0] < self.cols and 0 <= node[1] < self.rows:
|
||||
self._reserved.setdefault(node, []).append(claim)
|
||||
blocked.add(node)
|
||||
|
||||
def _escape_path(self, pin: Pin, net_no: int, conns, blocked: set[Node]) -> list[Node]:
|
||||
"""The shortest way from a pin to a cell clear of other nets' pads,
|
||||
against pads, keepouts and earlier escapes only (breadth first, on
|
||||
the pin's own layers, at most ``2 * _escape_cells`` long)."""
|
||||
starts = [n for n in self._end_nodes(pin, net_no, conns) if n not in blocked]
|
||||
came: dict[Node, Node | None] = dict.fromkeys(starts)
|
||||
frontier = list(starts)
|
||||
limit = 2 * self._escape_cells
|
||||
for _depth in range(limit):
|
||||
nxt: list[Node] = []
|
||||
for node in frontier:
|
||||
if node not in starts and self._open_ground(node, net_no):
|
||||
path = [node]
|
||||
while came[path[-1]] is not None:
|
||||
path.append(came[path[-1]])
|
||||
return path[::-1]
|
||||
x, y, layer = node
|
||||
for dx, dy in self._neighbours:
|
||||
n = (x + dx, y + dy, layer)
|
||||
if n in came or n in blocked:
|
||||
continue
|
||||
if not (0 <= n[0] < self.cols and 0 <= n[1] < self.rows):
|
||||
continue
|
||||
if not self._static_open(n, net_no):
|
||||
continue
|
||||
came[n] = node
|
||||
nxt.append(n)
|
||||
frontier = nxt
|
||||
if not frontier:
|
||||
break
|
||||
return []
|
||||
|
||||
def _open_ground(self, node: Node, net_no: int) -> bool:
|
||||
"""Clear of every other net's pad by a full trace spacing all round."""
|
||||
x, y, layer = node
|
||||
for dx, dy in self._halo_offsets:
|
||||
n = (x + dx, y + dy, layer)
|
||||
if not (0 <= n[0] < self.cols and 0 <= n[1] < self.rows):
|
||||
return False
|
||||
if not self._static_open(n, net_no):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _settle(self, conn: _Connection, delta: int) -> None:
|
||||
if not self._pending:
|
||||
return
|
||||
@@ -1021,12 +1005,8 @@ class GridRouter:
|
||||
self._conns = conns
|
||||
if not conns:
|
||||
return RouteResult(self.half_width)
|
||||
if self.access_cost or self.reserve_escapes:
|
||||
self._count_pending(conns)
|
||||
if self.access_cost:
|
||||
self._build_doorways(conns)
|
||||
if self.reserve_escapes:
|
||||
self._reserve_escapes(conns)
|
||||
|
||||
passes = self.max_passes if self.rip_up else 1
|
||||
best_count = -1
|
||||
@@ -1065,11 +1045,41 @@ class GridRouter:
|
||||
path = self._search(conn, allow_ripup=True, conns=conns)
|
||||
if path is None:
|
||||
return False
|
||||
for idx in sorted(self._crossed_owners(path, conn.net_no, conns)):
|
||||
self._rip(conns[idx])
|
||||
victims = [conns[i] for i in sorted(self._crossed_owners(path, conn.net_no, conns))]
|
||||
if self.rollback:
|
||||
return self._ripup_transaction(conn, path, victims, conns)
|
||||
for victim in victims:
|
||||
self._rip(victim)
|
||||
self._commit(conn, path)
|
||||
return True
|
||||
|
||||
def _ripup_transaction(self, conn, path, victims, conns) -> bool:
|
||||
"""Route ``conn`` through ``victims``, reroute them at once in free
|
||||
space, and undo it all if fewer connections end up routed."""
|
||||
saved = [(v, v.path, v.a_end, v.b_end, v.rip_count) for v in victims]
|
||||
own_rips = conn.rip_count
|
||||
for victim in victims:
|
||||
self._rip(victim)
|
||||
self._commit(conn, path)
|
||||
rerouted = 0
|
||||
for victim in victims:
|
||||
again = self._search(victim, allow_ripup=False, conns=conns)
|
||||
if again is not None:
|
||||
self._commit(victim, again)
|
||||
rerouted += 1
|
||||
if 1 + rerouted >= len(victims):
|
||||
return True # a gain, or an even trade that may pay off later
|
||||
for victim in victims:
|
||||
if victim.routed:
|
||||
self._rip(victim)
|
||||
self._rip(conn)
|
||||
conn.rip_count = own_rips
|
||||
for victim, old_path, a_end, b_end, rips in saved:
|
||||
victim.a_end, victim.b_end = a_end, b_end
|
||||
self._commit(victim, old_path)
|
||||
victim.rip_count = rips
|
||||
return False
|
||||
|
||||
def _build_connections(self) -> list[_Connection]:
|
||||
pins_by_net = self._pins_by_net()
|
||||
zones_by_net: dict[int, list] = {}
|
||||
@@ -1273,7 +1283,7 @@ def route_board(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -1292,6 +1302,6 @@ def route_board(
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
reserve_escapes=reserve_escapes,
|
||||
rollback=rollback,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
@@ -83,7 +83,7 @@ def route_dsn_board(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> tuple[RouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||
@@ -107,7 +107,7 @@ def route_dsn_board(
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
reserve_escapes=reserve_escapes,
|
||||
rollback=rollback,
|
||||
)
|
||||
return result, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -151,7 +151,7 @@ def build_routing_result(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||
route, scale, layer_names = route_dsn_board(
|
||||
@@ -159,7 +159,7 @@ def build_routing_result(
|
||||
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
reserve_escapes=reserve_escapes,
|
||||
rollback=rollback,
|
||||
)
|
||||
return _to_routing_result(route, dsn, scale, layer_names)
|
||||
|
||||
@@ -178,7 +178,7 @@ def route_dsn_board_exact(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||
@@ -206,7 +206,7 @@ def route_dsn_board_exact(
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
reserve_escapes=reserve_escapes,
|
||||
rollback=rollback,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -225,7 +225,7 @@ def build_exact_routing_result(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
reserve_escapes: bool = False,
|
||||
rollback: bool = True,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(
|
||||
@@ -241,7 +241,7 @@ def build_exact_routing_result(
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
reserve_escapes=reserve_escapes,
|
||||
rollback=rollback,
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
@@ -515,6 +515,10 @@ def route_board_rooms(
|
||||
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
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"""--reserve-escapes: a crowded pin keeps a way out until it is routed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.route.grid_router import GridRouter
|
||||
from freeroute.route.pipeline import (
|
||||
_rule_clearance_dsn,
|
||||
_rule_width_dsn,
|
||||
build_exact_routing_result,
|
||||
)
|
||||
|
||||
SMD = (Path(__file__).resolve().parent.parent / "dsn" / "fixtures" / "smd_demo.dsn").read_text()
|
||||
|
||||
|
||||
def _router():
|
||||
dsn = parse_dsn(SMD)
|
||||
s = dsn.resolution.value
|
||||
router = GridRouter(
|
||||
build_board(dsn),
|
||||
trace_width=round(_rule_width_dsn(dsn) * s),
|
||||
clearance=round(_rule_clearance_dsn(dsn) * s),
|
||||
orthogonal=True,
|
||||
resolution=2,
|
||||
reserve_escapes=True,
|
||||
)
|
||||
conns = router._build_connections()
|
||||
router._conns = conns
|
||||
router._count_pending(conns)
|
||||
router._reserve_escapes(conns)
|
||||
return router, conns
|
||||
|
||||
|
||||
def test_a_reserved_escape_is_closed_to_other_nets_and_open_to_its_own():
|
||||
router, conns = _router()
|
||||
assert router._reserved
|
||||
node, claims = next(iter(router._reserved.items()))
|
||||
pin_id, owner = claims[0]
|
||||
other = next(c.net_no for c in conns if c.net_no != owner)
|
||||
cell, layer = node[:2], node[2]
|
||||
assert router._enter_cost(cell, layer, other, True, conns) is None # even for rip-up
|
||||
assert router._enter_cost(cell, layer, owner, False, conns) is not None or node in router._keepout
|
||||
|
||||
|
||||
def test_the_escape_is_released_once_its_pin_is_routed():
|
||||
router, conns = _router()
|
||||
node, claims = next(iter(router._reserved.items()))
|
||||
other = next(c.net_no for c in conns if c.net_no != claims[0][1])
|
||||
cell, layer = node[:2], node[2]
|
||||
assert router._enter_cost(cell, layer, other, False, conns) is None
|
||||
for c in conns: # every connection routed: no pin is pending any more
|
||||
router._settle(c, -1)
|
||||
released = router._enter_cost(cell, layer, other, False, conns)
|
||||
reserved, router._reserved = router._reserved, {}
|
||||
assert released == router._enter_cost(cell, layer, other, False, conns)
|
||||
router._reserved = reserved
|
||||
|
||||
|
||||
def test_reserving_escapes_loses_nothing_on_the_demo_board():
|
||||
plain = build_exact_routing_result(parse_dsn(SMD), resolution=2)
|
||||
reserved = build_exact_routing_result(parse_dsn(SMD), resolution=2, reserve_escapes=True)
|
||||
assert len(reserved.unrouted) <= len(plain.unrouted)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Tracks and vias already on the board (the DSN's wiring) are fixed copper."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.route.pipeline import route_dsn_board_exact
|
||||
|
||||
_DSN = """(pcb wired (parser) (resolution um 10) (unit um)
|
||||
(structure
|
||||
(layer F.Cu (type signal)) (layer B.Cu (type signal))
|
||||
(boundary (path pcb 0 0 0 30000 0 30000 -20000 0 -20000 0 0))
|
||||
(via "Via[0-1]_600:300_um") (rule (width 250) (clearance 200)))
|
||||
(placement (component S (place A1 5000 -10000 front 0) (place A2 25000 -10000 front 0)))
|
||||
(library
|
||||
(image S (pin smd 1 0 0))
|
||||
(padstack smd (shape (rect F.Cu -400 -400 400 400)) (attach off))
|
||||
(padstack "Via[0-1]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off)))
|
||||
(network (net SIG (pins A1-1 A2-1)) (net GND))
|
||||
(wiring
|
||||
(wire (path F.Cu 400 15000 0 15000 -20000) (net GND) (type route))
|
||||
(via "Via[0-1]_600:300_um" 8000 -14000 (net GND) (type route))))"""
|
||||
|
||||
|
||||
def test_wiring_is_read_with_its_nets():
|
||||
dsn = parse_dsn(_DSN)
|
||||
assert [(w.net, w.layer, w.width) for w in dsn.wires] == [("GND", "F.Cu", 400.0)]
|
||||
assert [(v.net, v.x, v.y) for v in dsn.vias] == [("GND", 8000.0, -14000.0)]
|
||||
|
||||
|
||||
def test_wiring_is_fixed_copper_and_not_a_connection_to_route():
|
||||
board = build_board(parse_dsn(_DSN))
|
||||
copper = board.get_fixed_copper()
|
||||
gnd = next(n.net_number for n in board.nets if n.name == "GND")
|
||||
assert copper and all(net == gnd for net, _, _ in copper)
|
||||
assert not any(gnd in p.net_nos for p in board.get_pins())
|
||||
|
||||
|
||||
def test_other_nets_route_around_a_fixed_track():
|
||||
dsn = parse_dsn(_DSN)
|
||||
exact, scale, names = route_dsn_board_exact(dsn, resolution=2)
|
||||
sig = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "SIG")
|
||||
assert sig in exact.result.routed_net_numbers
|
||||
assert exact.drc_clean
|
||||
wall = 15000 * scale
|
||||
for layer, pts in exact.result.wires.get(sig, []):
|
||||
if names[layer] != "F.Cu":
|
||||
continue
|
||||
for p, q in zip(pts, pts[1:], strict=False):
|
||||
assert not (min(p.x, q.x) < wall < max(p.x, q.x)), "crossed the GND track on F.Cu"
|
||||
@@ -202,6 +202,18 @@ def test_smd_pads_reach_an_inner_plane_by_their_own_via():
|
||||
assert all(names[layer] in ("F.Cu", "B.Cu") for segs in result.result.wires.values() for layer, _ in segs)
|
||||
|
||||
|
||||
def test_a_pad_outside_the_plane_routes_to_it():
|
||||
# G1 sits left of a plane that covers only the right half, so its route
|
||||
# has to reach the plane's cells: those ends are the plane, not a pin
|
||||
dsn_text = _four_layer_dsn().replace(
|
||||
"(plane GND (polygon In1.Cu 0 500 -500 29500 -500 29500 -29500 500 -29500 500 -500))",
|
||||
"(plane GND (polygon In1.Cu 0 15000 -500 29500 -500 29500 -29500 15000 -29500 15000 -500))",
|
||||
)
|
||||
result, _scale, _names = route_dsn_board_exact(parse_dsn(dsn_text))
|
||||
gnd = next(i + 1 for i, n in enumerate(parse_dsn(dsn_text).nets) if n.name == "GND")
|
||||
assert gnd in result.result.routed_net_numbers
|
||||
|
||||
|
||||
def test_a_through_hole_pad_inside_a_plane_needs_no_route():
|
||||
from freeroute.route.grid_router import _connected_groups
|
||||
|
||||
@@ -211,3 +223,29 @@ def test_a_through_hole_pad_inside_a_plane_needs_no_route():
|
||||
groups = _connected_groups(gnd_pins, plane, frozenset(id(z) for z in plane))
|
||||
hub = next(g for g in groups if g[1])
|
||||
assert [p.component_ref for p in hub[0]] == ["GT"]
|
||||
|
||||
|
||||
def test_a_pad_inside_a_via_keepout_reaches_the_plane_by_a_via_outside_it():
|
||||
# a sensor's GND pad: vias are forbidden under the body, so the route
|
||||
# has to leave on the top layer and drop to the plane past the keepout
|
||||
dsn = parse_dsn("""(pcb repro (parser) (resolution um 10) (unit um)
|
||||
(structure
|
||||
(layer F.Cu (type signal)) (layer In1.Cu (type power)) (layer In2.Cu (type power))
|
||||
(layer B.Cu (type signal))
|
||||
(boundary (path pcb 0 0 0 20000 0 20000 -20000 0 -20000 0 0))
|
||||
(plane GND (polygon In1.Cu 0 500 -500 19500 -500 19500 -19500 500 -19500 500 -500))
|
||||
(via_keepout "" (polygon signal 0 8500 -8500 11500 -8500 11500 -11500 8500 -11500 8500 -8500))
|
||||
(via "Via[0-3]_600:300_um") (rule (width 150) (clearance 150)))
|
||||
(placement (component S (place G1 10000 -10000 front 0)))
|
||||
(library
|
||||
(image S (pin smd 1 0 0))
|
||||
(padstack smd (shape (rect F.Cu -175 -300 175 300)) (attach off))
|
||||
(padstack "Via[0-3]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off)))
|
||||
(network (net GND (pins G1-1))))""")
|
||||
exact, scale, _names = route_dsn_board_exact(dsn, resolution=2)
|
||||
assert exact.result.failed == []
|
||||
(via,) = exact.result.vias[1]
|
||||
half = 300 * scale # the via's radius
|
||||
outside = (via.x + half <= 8500 * scale or via.x - half >= 11500 * scale
|
||||
or via.y - half >= -8500 * scale or via.y + half <= -11500 * scale)
|
||||
assert outside, f"via at {via} is inside the via keepout"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""--rollback: a rip-up that loses connections is undone, leaving no trace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.route.grid_router import GridRouter
|
||||
from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board
|
||||
|
||||
FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures"
|
||||
|
||||
|
||||
def _routed_router(name):
|
||||
dsn = parse_dsn((FIXTURES / name).read_text())
|
||||
s = dsn.resolution.value
|
||||
router = GridRouter(
|
||||
build_board(dsn),
|
||||
trace_width=round(_rule_width_dsn(dsn) * s),
|
||||
clearance=round(_rule_clearance_dsn(dsn) * s),
|
||||
rollback=True,
|
||||
)
|
||||
return router, router.route()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["ripup_needed.dsn", "true_density.dsn", "kicad_routable.dsn"])
|
||||
def test_occupancy_only_holds_routed_connections(name):
|
||||
router, _ = _routed_router(name)
|
||||
conns = router._conns
|
||||
assert all(conns[i].routed for i in router._cell_owner.values())
|
||||
assert all(conns[i].routed for i in router._via_owner.values())
|
||||
assert all(conns[i].routed for claims in router._halo.values() for i in claims)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["ripup_needed.dsn", "true_density.dsn", "kicad_routable.dsn"])
|
||||
def test_rollback_never_routes_fewer_than_plain_rip_up(name):
|
||||
text = (FIXTURES / name).read_text()
|
||||
plain, _, _ = route_dsn_board(parse_dsn(text), rollback=False)
|
||||
undo, _, _ = route_dsn_board(parse_dsn(text))
|
||||
assert undo.unrouted <= plain.unrouted
|
||||
+1
-1
@@ -158,7 +158,7 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
|
||||
"orders": ["--orders", "2"],
|
||||
"promote": ["--promote-failures", "2"],
|
||||
"access": ["--access-cost", "5"],
|
||||
"escapes": ["--reserve-escapes"],
|
||||
"rollback": ["--no-rollback"],
|
||||
}
|
||||
for engine, options in ENGINE_OPTIONS.items():
|
||||
for option in options:
|
||||
|
||||
Reference in New Issue
Block a user