Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783dd8f0d1 | ||
|
|
8a82143517 | ||
|
|
8a50bde531 | ||
|
|
0f6a2a0812 | ||
|
|
245cd91c0e | ||
|
|
019b043cd5 | ||
|
|
a2e108bc51 | ||
|
|
3462089e09 | ||
|
|
35891c02ff |
@@ -70,6 +70,27 @@ class BasicBoard:
|
|||||||
def get_traces(self) -> list[Trace]:
|
def get_traces(self) -> list[Trace]:
|
||||||
return [it for it in self._items if isinstance(it, 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]:
|
def get_vias(self) -> list[Via]:
|
||||||
return [it for it in self._items if isinstance(it, 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 .board import BasicBoard
|
||||||
from .clearance import ClearanceMatrix
|
from .clearance import ClearanceMatrix
|
||||||
from .items import ConductionArea, ObstacleArea, Pin
|
from .items import ConductionArea, ObstacleArea, Pin, Trace, Via
|
||||||
from .layer import Layer, LayerStructure
|
from .layer import Layer, LayerStructure
|
||||||
from .net import Nets
|
from .net import Nets
|
||||||
from .transform import CoordinateTransform
|
from .transform import CoordinateTransform
|
||||||
@@ -56,6 +56,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
|||||||
_insert_keepouts(dsn, board, transform)
|
_insert_keepouts(dsn, board, transform)
|
||||||
_insert_image_keepouts(dsn, board, transform)
|
_insert_image_keepouts(dsn, board, transform)
|
||||||
_insert_zones(dsn, board, transform, nets)
|
_insert_zones(dsn, board, transform, nets)
|
||||||
|
_insert_wiring(dsn, board, transform, nets)
|
||||||
return board
|
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()]
|
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:
|
def _insert_zones(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
||||||
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
||||||
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ class Via(Item):
|
|||||||
|
|
||||||
padstack_name: str = ""
|
padstack_name: str = ""
|
||||||
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
|
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
|
@dataclass
|
||||||
|
|||||||
+14
-2
@@ -30,8 +30,8 @@ from freeroute.ses import write_ses
|
|||||||
#: which optional flags each engine actually implements. Anything not listed for
|
#: which optional flags each engine actually implements. Anything not listed for
|
||||||
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
||||||
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
||||||
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote", "access"}),
|
"grid": frozenset({"rip_up", "max_passes", "seed", "orders", "promote", "access", "rollback"}),
|
||||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access"}),
|
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed", "orders", "promote", "access", "rollback"}),
|
||||||
"room": frozenset({"shove", "pack"}),
|
"room": frozenset({"shove", "pack"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ _FLAG_NAMES = {
|
|||||||
"orders": "--orders",
|
"orders": "--orders",
|
||||||
"promote": "--promote-failures",
|
"promote": "--promote-failures",
|
||||||
"access": "--access-cost",
|
"access": "--access-cost",
|
||||||
|
"rollback": "--no-rollback",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -166,6 +167,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
help="grid and exact engines: extra cost for a route to pass where it would close "
|
help="grid and exact engines: extra cost for a route to pass where it would close "
|
||||||
"another pin's way out while that pin still has connections to route (default 0: off)",
|
"another pin's way out while that pin still has connections to route (default 0: off)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--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(
|
parser.add_argument(
|
||||||
"--jobs",
|
"--jobs",
|
||||||
type=int,
|
type=int,
|
||||||
@@ -195,6 +204,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
("orders", args.orders is not None),
|
("orders", args.orders is not None),
|
||||||
("promote", args.promote_failures is not None),
|
("promote", args.promote_failures is not None),
|
||||||
("access", args.access_cost is not None),
|
("access", args.access_cost is not None),
|
||||||
|
("rollback", not args.rollback),
|
||||||
)
|
)
|
||||||
if asked
|
if asked
|
||||||
]
|
]
|
||||||
@@ -229,6 +239,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
"resolution": 2 if args.grid_resolution is None else args.grid_resolution,
|
"resolution": 2 if args.grid_resolution is None else args.grid_resolution,
|
||||||
"heuristic_weight": args.heuristic_weight,
|
"heuristic_weight": args.heuristic_weight,
|
||||||
"access_cost": args.access_cost or 0.0,
|
"access_cost": args.access_cost or 0.0,
|
||||||
|
"rollback": args.rollback,
|
||||||
}
|
}
|
||||||
elif args.engine == "grid":
|
elif args.engine == "grid":
|
||||||
options = {
|
options = {
|
||||||
@@ -238,6 +249,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
"edge_clearance_mm": args.edge_clearance,
|
"edge_clearance_mm": args.edge_clearance,
|
||||||
"resolution": 1 if args.grid_resolution is None else args.grid_resolution,
|
"resolution": 1 if args.grid_resolution is None else args.grid_resolution,
|
||||||
"access_cost": args.access_cost or 0.0,
|
"access_cost": args.access_cost or 0.0,
|
||||||
|
"rollback": args.rollback,
|
||||||
}
|
}
|
||||||
if args.engine == "room":
|
if args.engine == "room":
|
||||||
result = build_rooms_routing_result(
|
result = build_rooms_routing_result(
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ __all__ = [
|
|||||||
"NetPin",
|
"NetPin",
|
||||||
"Net",
|
"Net",
|
||||||
"NetClass",
|
"NetClass",
|
||||||
|
"WiringVia",
|
||||||
|
"WiringWire",
|
||||||
"Keepout",
|
"Keepout",
|
||||||
"DsnBoard",
|
"DsnBoard",
|
||||||
]
|
]
|
||||||
@@ -186,6 +188,26 @@ class Keepout:
|
|||||||
kind: str = "keepout" # keepout | via_keepout | wire_keepout | place_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
|
@dataclass
|
||||||
class DsnBoard:
|
class DsnBoard:
|
||||||
"""The fully parsed DSN file."""
|
"""The fully parsed DSN file."""
|
||||||
@@ -197,6 +219,9 @@ class DsnBoard:
|
|||||||
layers: list[Layer] = field(default_factory=list)
|
layers: list[Layer] = field(default_factory=list)
|
||||||
#: copper zones, as planes of a net
|
#: copper zones, as planes of a net
|
||||||
planes: list[Plane] = field(default_factory=list)
|
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
|
#: bounding-box boundary shape (layer ``pcb``), if present
|
||||||
boundary: Shape | None = None
|
boundary: Shape | None = None
|
||||||
#: outline shapes (layer ``signal``)
|
#: outline shapes (layer ``signal``)
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from .model import (
|
|||||||
Pin,
|
Pin,
|
||||||
Resolution,
|
Resolution,
|
||||||
WidthRule,
|
WidthRule,
|
||||||
|
WiringVia,
|
||||||
|
WiringWire,
|
||||||
)
|
)
|
||||||
from .sexp import SExp, parse
|
from .sexp import SExp, parse
|
||||||
from .shapes import Shape, read_area, read_shape
|
from .shapes import Shape, read_area, read_shape
|
||||||
@@ -531,11 +533,45 @@ def parse_dsn(text: str) -> DsnBoard:
|
|||||||
_read_placement(it, board)
|
_read_placement(it, board)
|
||||||
elif head == "network":
|
elif head == "network":
|
||||||
_read_network(it, board)
|
_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
|
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):
|
def _read_plane(node: SExp):
|
||||||
"""``(plane <net> (polygon <layer> <width> x y ...))`` -> :class:`Plane`."""
|
"""``(plane <net> (polygon <layer> <width> x y ...))`` -> :class:`Plane`."""
|
||||||
from .model import Plane
|
from .model import Plane
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ def route_board_exact(
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> ExactRouteResult:
|
) -> ExactRouteResult:
|
||||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||||
|
|
||||||
@@ -129,6 +130,7 @@ def route_board_exact(
|
|||||||
seed=seed,
|
seed=seed,
|
||||||
route_first=route_first,
|
route_first=route_first,
|
||||||
access_cost=access_cost,
|
access_cost=access_cost,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
edge = 0 if edge_clearance is None else edge_clearance
|
edge = 0 if edge_clearance is None else edge_clearance
|
||||||
outline = board.outline
|
outline = board.outline
|
||||||
@@ -147,6 +149,10 @@ def route_board_exact(
|
|||||||
pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
||||||
if pin_layers:
|
if pin_layers:
|
||||||
tree.insert(TreeShape(next(owner), net, pin_layers, box))
|
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():
|
for obstacle in board.get_obstacle_areas():
|
||||||
if obstacle.layer not in signal_layers:
|
if obstacle.layer not in signal_layers:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class _Connection:
|
|||||||
"b_group",
|
"b_group",
|
||||||
"a_end",
|
"a_end",
|
||||||
"b_end",
|
"b_end",
|
||||||
|
"path",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
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.b_group: tuple[list[Pin], list] = ([b], [])
|
||||||
self.a_end: Pin | None = a
|
self.a_end: Pin | None = a
|
||||||
self.b_end: Pin | None = b
|
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:
|
class GridRouter:
|
||||||
@@ -159,6 +162,7 @@ class GridRouter:
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.board = board
|
self.board = board
|
||||||
# Pin access: while a pin still has connections to route, the cells
|
# Pin access: while a pin still has connections to route, the cells
|
||||||
@@ -168,6 +172,12 @@ class GridRouter:
|
|||||||
self.access_cost = max(float(access_cost), 0.0)
|
self.access_cost = max(float(access_cost), 0.0)
|
||||||
self._doorways: dict[Node, list[tuple[int, int]]] = {}
|
self._doorways: dict[Node, list[tuple[int, int]]] = {}
|
||||||
self._pending: dict[int, int] = {}
|
self._pending: dict[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
|
# None keeps the deterministic order; a seed shuffles which connection
|
||||||
# routes first, to measure how much a result owes to the order
|
# routes first, to measure how much a result owes to the order
|
||||||
self.seed = seed
|
self.seed = seed
|
||||||
@@ -240,6 +250,7 @@ class GridRouter:
|
|||||||
# trace halos: node -> indices of the connections claiming it
|
# trace halos: node -> indices of the connections claiming it
|
||||||
self._halo: dict[Node, list[int]] = {}
|
self._halo: dict[Node, list[int]] = {}
|
||||||
self._rasterize_obstacles()
|
self._rasterize_obstacles()
|
||||||
|
self._rasterize_fixed_copper()
|
||||||
|
|
||||||
# --- grid helpers -------------------------------------------------------
|
# --- grid helpers -------------------------------------------------------
|
||||||
|
|
||||||
@@ -300,6 +311,9 @@ class GridRouter:
|
|||||||
self._via_edge_block: set[Cell] = set()
|
self._via_edge_block: set[Cell] = set()
|
||||||
# wire keepouts as boxes, to check a pin stub the way the exact engine will
|
# wire keepouts as boxes, to check a pin stub the way the exact engine will
|
||||||
self._keepout_boxes: dict[int, list[tuple[IntBox, bool]]] = {}
|
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
|
# pin end nodes no wire can reach without entering a keepout
|
||||||
self._via_only: set[Node] = set()
|
self._via_only: set[Node] = set()
|
||||||
if outline is not None:
|
if outline is not None:
|
||||||
@@ -349,6 +363,18 @@ class GridRouter:
|
|||||||
self._keepout.add((*cell, item.layer))
|
self._keepout.add((*cell, item.layer))
|
||||||
if item.blocks_vias:
|
if item.blocks_vias:
|
||||||
self._via_keepout.add((*cell, item.layer))
|
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 -----------------------------------------------------
|
# --- occupancy cost -----------------------------------------------------
|
||||||
|
|
||||||
@@ -431,7 +457,14 @@ class GridRouter:
|
|||||||
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
||||||
if not starts or not goals:
|
if not starts or not goals:
|
||||||
return None
|
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:
|
if common:
|
||||||
conn.a_end, conn.b_end = starts[common[0]], goals[common[0]]
|
conn.a_end, conn.b_end = starts[common[0]], goals[common[0]]
|
||||||
return [common[0]]
|
return [common[0]]
|
||||||
@@ -441,10 +474,11 @@ class GridRouter:
|
|||||||
g_score: dict[Node, float] = {}
|
g_score: dict[Node, float] = {}
|
||||||
came_from: dict[Node, Node] = {}
|
came_from: dict[Node, Node] = {}
|
||||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
# 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):
|
for s in sorted(starts):
|
||||||
pin = starts[s]
|
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))
|
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
|
# Via footprints are checked lazily: a layer change goes on the heap as
|
||||||
@@ -463,6 +497,12 @@ class GridRouter:
|
|||||||
ok = via_ok.get(cell)
|
ok = via_ok.get(cell)
|
||||||
if ok is None:
|
if ok is None:
|
||||||
ok = via_ok[cell] = self._via_placeable(cell, net_no, conns)
|
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:
|
if ok:
|
||||||
self._relax(source, target, g_via, goal_cell, g_score, came_from, open_heap)
|
self._relax(source, target, g_via, goal_cell, g_score, came_from, open_heap)
|
||||||
continue
|
continue
|
||||||
@@ -526,6 +566,29 @@ class GridRouter:
|
|||||||
# copper too, or a stub ends up closer than clearance to a trace in the
|
# copper too, or a stub ends up closer than clearance to a trace in the
|
||||||
# cell beside it.
|
# 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]:
|
def _stub_cells(self, pin_location: IntPoint, cell: Cell) -> list[Cell]:
|
||||||
"""Cells where another net's trace would come too close to the stub."""
|
"""Cells where another net's trace would come too close to the stub."""
|
||||||
from freeroute.board.outline import _point_segment
|
from freeroute.board.outline import _point_segment
|
||||||
@@ -788,6 +851,7 @@ class GridRouter:
|
|||||||
# --- commit / rip -------------------------------------------------------
|
# --- commit / rip -------------------------------------------------------
|
||||||
|
|
||||||
def _commit(self, conn: _Connection, path: list[Node]) -> None:
|
def _commit(self, conn: _Connection, path: list[Node]) -> None:
|
||||||
|
conn.path = list(path)
|
||||||
wire_segs, via_cells = _split_path(path)
|
wire_segs, via_cells = _split_path(path)
|
||||||
via_cells = list(via_cells)
|
via_cells = list(via_cells)
|
||||||
if isinstance(conn.a_end, ConductionArea):
|
if isinstance(conn.a_end, ConductionArea):
|
||||||
@@ -981,11 +1045,41 @@ class GridRouter:
|
|||||||
path = self._search(conn, allow_ripup=True, conns=conns)
|
path = self._search(conn, allow_ripup=True, conns=conns)
|
||||||
if path is None:
|
if path is None:
|
||||||
return False
|
return False
|
||||||
for idx in sorted(self._crossed_owners(path, conn.net_no, conns)):
|
victims = [conns[i] for i in sorted(self._crossed_owners(path, conn.net_no, conns))]
|
||||||
self._rip(conns[idx])
|
if self.rollback:
|
||||||
|
return self._ripup_transaction(conn, path, victims, conns)
|
||||||
|
for victim in victims:
|
||||||
|
self._rip(victim)
|
||||||
self._commit(conn, path)
|
self._commit(conn, path)
|
||||||
return True
|
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]:
|
def _build_connections(self) -> list[_Connection]:
|
||||||
pins_by_net = self._pins_by_net()
|
pins_by_net = self._pins_by_net()
|
||||||
zones_by_net: dict[int, list] = {}
|
zones_by_net: dict[int, list] = {}
|
||||||
@@ -1189,6 +1283,7 @@ def route_board(
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> RouteResult:
|
) -> RouteResult:
|
||||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||||
router = GridRouter(
|
router = GridRouter(
|
||||||
@@ -1207,5 +1302,6 @@ def route_board(
|
|||||||
seed=seed,
|
seed=seed,
|
||||||
route_first=route_first,
|
route_first=route_first,
|
||||||
access_cost=access_cost,
|
access_cost=access_cost,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
return router.route()
|
return router.route()
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ def route_dsn_board(
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> tuple[RouteResult, int, list[str]]:
|
) -> tuple[RouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||||
@@ -106,6 +107,7 @@ def route_dsn_board(
|
|||||||
seed=seed,
|
seed=seed,
|
||||||
route_first=route_first,
|
route_first=route_first,
|
||||||
access_cost=access_cost,
|
access_cost=access_cost,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
return result, scale, [layer.name for layer in dsn.layers]
|
return result, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
@@ -149,6 +151,7 @@ def build_routing_result(
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> RoutingResult:
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||||
route, scale, layer_names = route_dsn_board(
|
route, scale, layer_names = route_dsn_board(
|
||||||
@@ -156,6 +159,7 @@ def build_routing_result(
|
|||||||
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
||||||
route_first=route_first,
|
route_first=route_first,
|
||||||
access_cost=access_cost,
|
access_cost=access_cost,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
return _to_routing_result(route, dsn, scale, layer_names)
|
return _to_routing_result(route, dsn, scale, layer_names)
|
||||||
|
|
||||||
@@ -174,6 +178,7 @@ def route_dsn_board_exact(
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||||
@@ -201,6 +206,7 @@ def route_dsn_board_exact(
|
|||||||
seed=seed,
|
seed=seed,
|
||||||
route_first=route_first,
|
route_first=route_first,
|
||||||
access_cost=access_cost,
|
access_cost=access_cost,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
return exact, scale, [layer.name for layer in dsn.layers]
|
return exact, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
@@ -219,6 +225,7 @@ def build_exact_routing_result(
|
|||||||
seed: int | None = None,
|
seed: int | None = None,
|
||||||
route_first: frozenset | None = None,
|
route_first: frozenset | None = None,
|
||||||
access_cost: float = 0.0,
|
access_cost: float = 0.0,
|
||||||
|
rollback: bool = True,
|
||||||
) -> RoutingResult:
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||||
exact, scale, layer_names = route_dsn_board_exact(
|
exact, scale, layer_names = route_dsn_board_exact(
|
||||||
@@ -234,6 +241,7 @@ def build_exact_routing_result(
|
|||||||
seed=seed,
|
seed=seed,
|
||||||
route_first=route_first,
|
route_first=route_first,
|
||||||
access_cost=access_cost,
|
access_cost=access_cost,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
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)
|
pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
||||||
if pin_layers:
|
if pin_layers:
|
||||||
tree.insert(TreeShape(next(owner), net, pin_layers, box))
|
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():
|
for obstacle in board.get_obstacle_areas():
|
||||||
if obstacle.layer not in signal_layers:
|
if obstacle.layer not in signal_layers:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -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)
|
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():
|
def test_a_through_hole_pad_inside_a_plane_needs_no_route():
|
||||||
from freeroute.route.grid_router import _connected_groups
|
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))
|
groups = _connected_groups(gnd_pins, plane, frozenset(id(z) for z in plane))
|
||||||
hub = next(g for g in groups if g[1])
|
hub = next(g for g in groups if g[1])
|
||||||
assert [p.component_ref for p in hub[0]] == ["GT"]
|
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
|
||||||
@@ -158,6 +158,7 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
|
|||||||
"orders": ["--orders", "2"],
|
"orders": ["--orders", "2"],
|
||||||
"promote": ["--promote-failures", "2"],
|
"promote": ["--promote-failures", "2"],
|
||||||
"access": ["--access-cost", "5"],
|
"access": ["--access-cost", "5"],
|
||||||
|
"rollback": ["--no-rollback"],
|
||||||
}
|
}
|
||||||
for engine, options in ENGINE_OPTIONS.items():
|
for engine, options in ENGINE_OPTIONS.items():
|
||||||
for option in options:
|
for option in options:
|
||||||
|
|||||||
Reference in New Issue
Block a user