Add multi-layer routing with vias
Extends the grid router with a layer axis: A* nodes are (col, row, layer), in-plane moves stay on a layer, and a via move transitions between layers at a cell for a configurable via_cost (so the router prefers one layer but changes layers to get through). Occupancy is tracked per (cell, layer); a through via must be clear on every signal layer and then blocks all of them for other nets. RouteResult now carries per-layer wire segments and per-net via locations. The pipeline emits each segment on its layer and each via as (via <padstack> x y), using the DSN's via padstack; wire and via coordinates are converted from board units back to DSN units. route() and route_dsn_board() take an optional layers= to restrict routing (e.g. a single layer) for comparison.
This commit is contained in:
parent
89c0481ccc
commit
992bba82a0
@ -1,21 +1,20 @@
|
|||||||
"""A grid-based maze router (MVP).
|
"""A grid-based maze router (MVP), now multi-layer with vias.
|
||||||
|
|
||||||
This is an *MVP* autorouter, not a port of FreeRouting's expansion-room maze.
|
Still an *MVP* — not a port of FreeRouting's expansion-room maze — but it routes
|
||||||
Per the phase brief the milestone is connectivity parity with the reference JAR
|
across the board's signal layers and changes layers through vias when a net
|
||||||
on a simple board, not FreeRouting-quality optimization. It routes on a single
|
cannot get through on one layer:
|
||||||
signal layer with an A* search over a uniform occupancy grid:
|
|
||||||
|
|
||||||
* a cell is blocked if it overlaps another net's pad or a keepout, inflated by
|
* the occupancy grid has a layer axis; A* nodes are ``(col, row, layer)``;
|
||||||
``clearance + half trace width``;
|
* in-plane moves (8-connected, 45-degree) cost distance and stay on a layer;
|
||||||
* each net's ratsnest is connected pin-to-pin; a routed trace's cells then block
|
* a **via move** transitions between layers at a cell for a configurable
|
||||||
other nets (so routes do not overlap);
|
``via_cost`` (so the router prefers one layer but will change to get through);
|
||||||
* the resulting cell path is turned into a trace polyline whose endpoints are the
|
* a cell is blocked per-layer by other-net pads/traces/vias on that layer plus
|
||||||
exact pin locations.
|
keepouts; a via cell must be clear on **all** signal layers (a through via);
|
||||||
|
* each net's ratsnest is connected pin-to-pin; routed traces and vias then block
|
||||||
|
other nets. Trace endpoints land exactly on the connected pads.
|
||||||
|
|
||||||
Deferred (router phase, exact-geometry track): FreeRouting's free-space
|
Deferred (still): FreeRouting's free-space expansion rooms, rip-up-and-retry,
|
||||||
expansion rooms, rip-up-and-retry, multi-layer via search, and shove. Those
|
blind/buried via spans, and shove. Those raise coverage/quality on dense boards.
|
||||||
raise quality/completeness; this reaches connectivity on boards whose nets route
|
|
||||||
on one layer without crossing.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -29,7 +28,7 @@ from freeroute.geometry import IntPoint
|
|||||||
|
|
||||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||||
|
|
||||||
# 8-connected neighbourhood (orthogonal + diagonal) for 45-degree routing.
|
# 8-connected in-plane neighbourhood (orthogonal + diagonal) for 45-degree routing.
|
||||||
_NEIGHBOURS = [
|
_NEIGHBOURS = [
|
||||||
(1, 0),
|
(1, 0),
|
||||||
(-1, 0),
|
(-1, 0),
|
||||||
@ -41,31 +40,43 @@ _NEIGHBOURS = [
|
|||||||
(-1, -1),
|
(-1, -1),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Cell = tuple[int, int]
|
||||||
|
Node = tuple[int, int, int] # (col, row, layer)
|
||||||
|
|
||||||
|
|
||||||
class RouteResult:
|
class RouteResult:
|
||||||
"""Per-net routing outcome in *board* units.
|
"""Per-net routing outcome in *board* units.
|
||||||
|
|
||||||
``wires`` maps a net number to a list of trace paths (each a list of
|
``wires`` maps a net number to a list of ``(layer_index, [IntPoint, ...])``
|
||||||
:class:`IntPoint`). ``routed_net_numbers`` are the nets that got >= 1 trace.
|
trace segments; ``vias`` maps a net number to a list of via locations.
|
||||||
|
``routed_net_numbers`` are the nets that got any wire or via.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("wires", "half_width", "layer")
|
__slots__ = ("wires", "vias", "half_width")
|
||||||
|
|
||||||
def __init__(self, half_width: int, layer: int) -> None:
|
def __init__(self, half_width: int) -> None:
|
||||||
self.wires: dict[int, list[list[IntPoint]]] = {}
|
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
|
||||||
|
self.vias: dict[int, list[IntPoint]] = {}
|
||||||
self.half_width = half_width
|
self.half_width = half_width
|
||||||
self.layer = layer
|
|
||||||
|
|
||||||
def add(self, net_no: int, path: list[IntPoint]) -> None:
|
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
|
||||||
self.wires.setdefault(net_no, []).append(path)
|
self.wires.setdefault(net_no, []).append((layer, path))
|
||||||
|
|
||||||
|
def add_via(self, net_no: int, location: IntPoint) -> None:
|
||||||
|
self.vias.setdefault(net_no, []).append(location)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def routed_net_numbers(self) -> set[int]:
|
def routed_net_numbers(self) -> set[int]:
|
||||||
return {n for n, paths in self.wires.items() if paths}
|
nets = {n for n, w in self.wires.items() if w}
|
||||||
|
nets |= {n for n, v in self.vias.items() if v}
|
||||||
|
return nets
|
||||||
|
|
||||||
|
def via_count(self) -> int:
|
||||||
|
return sum(len(v) for v in self.vias.values())
|
||||||
|
|
||||||
|
|
||||||
class GridRouter:
|
class GridRouter:
|
||||||
"""Routes a :class:`BasicBoard` on one layer with a uniform-grid A* search."""
|
"""Routes a :class:`BasicBoard` across signal layers with a grid A* + vias."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -73,39 +84,45 @@ class GridRouter:
|
|||||||
*,
|
*,
|
||||||
trace_width: int,
|
trace_width: int,
|
||||||
clearance: int,
|
clearance: int,
|
||||||
layer: int = 0,
|
layers: list[int] | None = None,
|
||||||
|
via_cost: float = 10.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.board = board
|
self.board = board
|
||||||
self.trace_width = max(trace_width, 1)
|
self.trace_width = max(trace_width, 1)
|
||||||
self.clearance = max(clearance, 0)
|
self.clearance = max(clearance, 0)
|
||||||
self.layer = layer
|
|
||||||
self.half_width = self.trace_width // 2
|
self.half_width = self.trace_width // 2
|
||||||
|
self.via_cost = via_cost
|
||||||
# cell size must fit a trace plus its clearance to a neighbouring trace
|
# cell size must fit a trace plus its clearance to a neighbouring trace
|
||||||
self.step = max(self.trace_width + self.clearance, 1)
|
self.step = max(self.trace_width + self.clearance, 1)
|
||||||
|
|
||||||
|
if layers is None:
|
||||||
|
layers = [i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal]
|
||||||
|
self.layers = list(layers)
|
||||||
|
|
||||||
box = board.bounding_box
|
box = board.bounding_box
|
||||||
self.origin_x = box.ll.x
|
self.origin_x = box.ll.x
|
||||||
self.origin_y = box.ll.y
|
self.origin_y = box.ll.y
|
||||||
self.cols = max(1, math.ceil((box.ur.x - box.ll.x) / self.step) + 1)
|
self.cols = max(1, math.ceil((box.ur.x - box.ll.x) / self.step) + 1)
|
||||||
self.rows = max(1, math.ceil((box.ur.y - box.ll.y) / self.step) + 1)
|
self.rows = max(1, math.ceil((box.ur.y - box.ll.y) / self.step) + 1)
|
||||||
|
|
||||||
# cell -> set of net numbers whose pad blocks it; keepout cells; trace cells
|
# per-(cell, layer) occupancy
|
||||||
self._pad_block: dict[tuple[int, int], set[int]] = {}
|
self._pad_block: dict[Node, set[int]] = {}
|
||||||
self._keepout: set[tuple[int, int]] = set()
|
self._keepout: set[Node] = set()
|
||||||
self._trace_block: dict[tuple[int, int], int] = {}
|
self._trace_block: dict[Node, int] = {}
|
||||||
|
self._via_block: dict[Cell, int] = {}
|
||||||
self._rasterize_obstacles()
|
self._rasterize_obstacles()
|
||||||
|
|
||||||
# --- grid helpers -------------------------------------------------------
|
# --- grid helpers -------------------------------------------------------
|
||||||
|
|
||||||
def _cell_of(self, point: IntPoint) -> tuple[int, int]:
|
def _cell_of(self, point: IntPoint) -> Cell:
|
||||||
gx = round((point.x - self.origin_x) / self.step)
|
gx = round((point.x - self.origin_x) / self.step)
|
||||||
gy = round((point.y - self.origin_y) / self.step)
|
gy = round((point.y - self.origin_y) / self.step)
|
||||||
gx = min(max(gx, 0), self.cols - 1)
|
gx = min(max(gx, 0), self.cols - 1)
|
||||||
gy = min(max(gy, 0), self.rows - 1)
|
gy = min(max(gy, 0), self.rows - 1)
|
||||||
return gx, gy
|
return gx, gy
|
||||||
|
|
||||||
def _cell_center(self, gx: int, gy: int) -> IntPoint:
|
def _cell_center(self, cell: Cell) -> IntPoint:
|
||||||
return IntPoint(self.origin_x + gx * self.step, self.origin_y + gy * self.step)
|
return IntPoint(self.origin_x + cell[0] * self.step, self.origin_y + cell[1] * self.step)
|
||||||
|
|
||||||
def _cells_in_box(self, box, margin: int):
|
def _cells_in_box(self, box, margin: int):
|
||||||
lo_x = round((box.ll.x - margin - self.origin_x) / self.step)
|
lo_x = round((box.ll.x - margin - self.origin_x) / self.step)
|
||||||
@ -120,113 +137,175 @@ class GridRouter:
|
|||||||
margin = self.clearance + self.half_width
|
margin = self.clearance + self.half_width
|
||||||
for item in self.board.get_items():
|
for item in self.board.get_items():
|
||||||
if isinstance(item, Pin):
|
if isinstance(item, Pin):
|
||||||
if self.layer not in item.layers:
|
|
||||||
continue
|
|
||||||
box = item.shape.bounding_box() if item.shape is not None else None
|
box = item.shape.bounding_box() if item.shape is not None else None
|
||||||
if box is None or box.is_empty():
|
if box is None or box.is_empty():
|
||||||
continue
|
continue
|
||||||
net = item.net_nos[0] if item.net_nos else 0
|
net = item.net_nos[0] if item.net_nos else 0
|
||||||
|
pin_layers = [layer for layer in item.layers if layer in self.layers]
|
||||||
for cell in self._cells_in_box(box, margin):
|
for cell in self._cells_in_box(box, margin):
|
||||||
self._pad_block.setdefault(cell, set()).add(net)
|
for layer in pin_layers:
|
||||||
|
self._pad_block.setdefault((*cell, layer), set()).add(net)
|
||||||
elif isinstance(item, ObstacleArea):
|
elif isinstance(item, ObstacleArea):
|
||||||
if item.layer != self.layer:
|
if item.layer not in self.layers:
|
||||||
continue
|
continue
|
||||||
box = item.bounding_box()
|
box = item.bounding_box()
|
||||||
if box.is_empty():
|
if box.is_empty():
|
||||||
continue
|
continue
|
||||||
for cell in self._cells_in_box(box, self.clearance):
|
for cell in self._cells_in_box(box, self.clearance):
|
||||||
self._keepout.add(cell)
|
self._keepout.add((*cell, item.layer))
|
||||||
|
|
||||||
def _blocked(self, cell: tuple[int, int], net_no: int) -> bool:
|
def _blocked(self, cell: Cell, layer: int, net_no: int) -> bool:
|
||||||
if cell in self._keepout:
|
node = (*cell, layer)
|
||||||
|
if node in self._keepout:
|
||||||
return True
|
return True
|
||||||
pads = self._pad_block.get(cell)
|
pads = self._pad_block.get(node)
|
||||||
if pads and any(n != net_no for n in pads):
|
if pads and any(n != net_no for n in pads):
|
||||||
return True
|
return True
|
||||||
trace = self._trace_block.get(cell)
|
trace = self._trace_block.get(node)
|
||||||
return trace is not None and trace != net_no
|
if trace is not None and trace != net_no:
|
||||||
|
return True
|
||||||
|
via = self._via_block.get(cell)
|
||||||
|
return via is not None and via != net_no
|
||||||
|
|
||||||
|
def _via_placeable(self, cell: Cell, net_no: int) -> bool:
|
||||||
|
"""A through via at ``cell`` needs every signal layer clear of other nets."""
|
||||||
|
return all(not self._blocked(cell, layer, net_no) for layer in self.layers)
|
||||||
|
|
||||||
# --- A* search ----------------------------------------------------------
|
# --- A* search ----------------------------------------------------------
|
||||||
|
|
||||||
def _search(
|
def _search(
|
||||||
self, start: tuple[int, int], goal: tuple[int, int], net_no: int
|
self, starts: set[Node], goal_cell: Cell, goals: set[Node], net_no: int
|
||||||
) -> list[tuple[int, int]] | None:
|
) -> list[Node] | None:
|
||||||
if start == goal:
|
if starts & goals:
|
||||||
return [start]
|
return [next(iter(starts & goals))]
|
||||||
open_heap: list[tuple[float, tuple[int, int]]] = []
|
open_heap: list[tuple[float, Node]] = []
|
||||||
heapq.heappush(open_heap, (0.0, start))
|
g_score: dict[Node, float] = {}
|
||||||
came_from: dict[tuple[int, int], tuple[int, int]] = {}
|
came_from: dict[Node, Node] = {}
|
||||||
g_score: dict[tuple[int, int], float] = {start: 0.0}
|
for s in starts:
|
||||||
|
g_score[s] = 0.0
|
||||||
def h(cell):
|
heapq.heappush(open_heap, (self._h(s, goal_cell), s))
|
||||||
return math.hypot(cell[0] - goal[0], cell[1] - goal[1])
|
|
||||||
|
|
||||||
while open_heap:
|
while open_heap:
|
||||||
_, current = heapq.heappop(open_heap)
|
_, current = heapq.heappop(open_heap)
|
||||||
if current == goal:
|
if current in goals:
|
||||||
return _reconstruct(came_from, current)
|
return _reconstruct(came_from, current)
|
||||||
cx, cy = current
|
cx, cy, cl = current
|
||||||
|
base = g_score[current]
|
||||||
|
# in-plane moves
|
||||||
for dx, dy in _NEIGHBOURS:
|
for dx, dy in _NEIGHBOURS:
|
||||||
nxt = (cx + dx, cy + dy)
|
cell = (cx + dx, cy + dy)
|
||||||
if not (0 <= nxt[0] < self.cols and 0 <= nxt[1] < self.rows):
|
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||||
continue
|
continue
|
||||||
# the goal cell may be blocked by its own pad's net-agnostic
|
nxt = (*cell, cl)
|
||||||
# rasterization; always allow stepping onto the goal
|
if nxt not in goals and self._blocked(cell, cl, net_no):
|
||||||
if nxt != goal and self._blocked(nxt, net_no):
|
|
||||||
continue
|
continue
|
||||||
step_cost = 1.0 if dx == 0 or dy == 0 else math.sqrt(2)
|
cost = 1.0 if dx == 0 or dy == 0 else math.sqrt(2)
|
||||||
tentative = g_score[current] + step_cost
|
self._relax(current, nxt, base + cost, goal_cell, g_score, came_from, open_heap)
|
||||||
if tentative < g_score.get(nxt, math.inf):
|
# via moves (change layer at the same cell)
|
||||||
came_from[nxt] = current
|
if len(self.layers) > 1 and self._via_placeable((cx, cy), net_no):
|
||||||
g_score[nxt] = tentative
|
for layer in self.layers:
|
||||||
heapq.heappush(open_heap, (tentative + h(nxt), nxt))
|
if layer == cl:
|
||||||
|
continue
|
||||||
|
nxt = (cx, cy, layer)
|
||||||
|
self._relax(
|
||||||
|
current,
|
||||||
|
nxt,
|
||||||
|
base + self.via_cost,
|
||||||
|
goal_cell,
|
||||||
|
g_score,
|
||||||
|
came_from,
|
||||||
|
open_heap,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _mark_trace(self, cells: list[tuple[int, int]], net_no: int) -> None:
|
def _relax(self, current, nxt, tentative, goal_cell, g_score, came_from, open_heap):
|
||||||
for cell in cells:
|
if tentative < g_score.get(nxt, math.inf):
|
||||||
self._trace_block[cell] = net_no
|
came_from[nxt] = current
|
||||||
|
g_score[nxt] = tentative
|
||||||
|
heapq.heappush(open_heap, (tentative + self._h(nxt, goal_cell), nxt))
|
||||||
|
|
||||||
|
def _h(self, node: Node, goal_cell: Cell) -> float:
|
||||||
|
return math.hypot(node[0] - goal_cell[0], node[1] - goal_cell[1])
|
||||||
|
|
||||||
|
# --- commit / marking ---------------------------------------------------
|
||||||
|
|
||||||
|
def _mark(self, wires, vias, net_no: int) -> None:
|
||||||
|
for layer, cells in wires:
|
||||||
|
for cell in cells:
|
||||||
|
self._trace_block[(*cell, layer)] = net_no
|
||||||
|
for cell in vias:
|
||||||
|
self._via_block[cell] = net_no
|
||||||
|
|
||||||
# --- public routing -----------------------------------------------------
|
# --- public routing -----------------------------------------------------
|
||||||
|
|
||||||
def route(self) -> RouteResult:
|
def route(self) -> RouteResult:
|
||||||
"""Route every net with >= 2 pins on this router's layer."""
|
"""Route every net with >= 2 pins across the router's signal layers."""
|
||||||
result = RouteResult(self.half_width, self.layer)
|
result = RouteResult(self.half_width)
|
||||||
pins_by_net = self._pins_by_net()
|
pins_by_net = self._pins_by_net()
|
||||||
# route nets with fewer pins first (usually easier / shorter)
|
|
||||||
for net_no in sorted(pins_by_net, key=lambda n: len(pins_by_net[n])):
|
for net_no in sorted(pins_by_net, key=lambda n: len(pins_by_net[n])):
|
||||||
pins = pins_by_net[net_no]
|
pins = pins_by_net[net_no]
|
||||||
if len(pins) < 2:
|
if len(pins) < 2:
|
||||||
continue
|
continue
|
||||||
for a, b in zip(pins, pins[1:], strict=False):
|
for a, b in zip(pins, pins[1:], strict=False):
|
||||||
path = self._route_connection(a, b, net_no)
|
self._route_connection(a, b, net_no, result)
|
||||||
if path is not None:
|
|
||||||
result.add(net_no, path)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
||||||
by_net: dict[int, list[Pin]] = {}
|
by_net: dict[int, list[Pin]] = {}
|
||||||
for pin in self.board.get_pins():
|
for pin in self.board.get_pins():
|
||||||
if self.layer not in pin.layers:
|
if not any(layer in self.layers for layer in pin.layers):
|
||||||
continue
|
continue
|
||||||
for net_no in pin.net_nos:
|
for net_no in pin.net_nos:
|
||||||
by_net.setdefault(net_no, []).append(pin)
|
by_net.setdefault(net_no, []).append(pin)
|
||||||
return by_net
|
return by_net
|
||||||
|
|
||||||
def _route_connection(self, a: Pin, b: Pin, net_no: int) -> list[IntPoint] | None:
|
def _route_connection(self, a: Pin, b: Pin, net_no: int, result: RouteResult) -> None:
|
||||||
start = self._cell_of(a.location)
|
start_cell = self._cell_of(a.location)
|
||||||
goal = self._cell_of(b.location)
|
goal_cell = self._cell_of(b.location)
|
||||||
cells = self._search(start, goal, net_no)
|
a_layers = [layer for layer in a.layers if layer in self.layers]
|
||||||
if cells is None:
|
b_layers = [layer for layer in b.layers if layer in self.layers]
|
||||||
return None
|
if not a_layers or not b_layers:
|
||||||
self._mark_trace(cells, net_no)
|
return
|
||||||
# cell centres, with exact pad locations as the true endpoints
|
starts = {(*start_cell, layer) for layer in a_layers}
|
||||||
points = [a.location]
|
goals = {(*goal_cell, layer) for layer in b_layers}
|
||||||
points.extend(self._cell_center(gx, gy) for gx, gy in cells)
|
path = self._search(starts, goal_cell, goals, net_no)
|
||||||
points.append(b.location)
|
if path is None:
|
||||||
return _simplify(points)
|
return
|
||||||
|
wires, vias = _split_path(path)
|
||||||
|
self._mark(wires, vias, net_no)
|
||||||
|
# emit wires with exact pad endpoints; emit vias at their cell centres
|
||||||
|
for idx, (layer, cells) in enumerate(wires):
|
||||||
|
points = [self._cell_center(c) for c in cells]
|
||||||
|
if idx == 0:
|
||||||
|
points.insert(0, a.location)
|
||||||
|
if idx == len(wires) - 1:
|
||||||
|
points.append(b.location)
|
||||||
|
points = _simplify(points)
|
||||||
|
if len(points) >= 2:
|
||||||
|
result.add_wire(net_no, layer, points)
|
||||||
|
for cell in vias:
|
||||||
|
result.add_via(net_no, self._cell_center(cell))
|
||||||
|
|
||||||
|
|
||||||
def _reconstruct(came_from, current):
|
def _split_path(path: list[Node]) -> tuple[list[tuple[int, list[Cell]]], list[Cell]]:
|
||||||
|
"""Split a node path into per-layer wire segments and via cells."""
|
||||||
|
wires: list[tuple[int, list[Cell]]] = []
|
||||||
|
vias: list[Cell] = []
|
||||||
|
i = 0
|
||||||
|
n = len(path)
|
||||||
|
while i < n:
|
||||||
|
layer = path[i][2]
|
||||||
|
cells: list[Cell] = []
|
||||||
|
while i < n and path[i][2] == layer:
|
||||||
|
cells.append((path[i][0], path[i][1]))
|
||||||
|
i += 1
|
||||||
|
wires.append((layer, cells))
|
||||||
|
if i < n: # a via connects this segment's last cell to the next layer
|
||||||
|
vias.append(cells[-1])
|
||||||
|
return wires, vias
|
||||||
|
|
||||||
|
|
||||||
|
def _reconstruct(came_from: dict[Node, Node], current: Node) -> list[Node]:
|
||||||
path = [current]
|
path = [current]
|
||||||
while current in came_from:
|
while current in came_from:
|
||||||
current = came_from[current]
|
current = came_from[current]
|
||||||
@ -243,7 +322,6 @@ def _simplify(points: list[IntPoint]) -> list[IntPoint]:
|
|||||||
continue
|
continue
|
||||||
if len(out) >= 2:
|
if len(out) >= 2:
|
||||||
a, b = out[-2], out[-1]
|
a, b = out[-2], out[-1]
|
||||||
# collinear if cross product of (b-a) and (p-a) is zero
|
|
||||||
if (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) == 0:
|
if (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) == 0:
|
||||||
out[-1] = p
|
out[-1] = p
|
||||||
continue
|
continue
|
||||||
@ -252,8 +330,19 @@ def _simplify(points: list[IntPoint]) -> list[IntPoint]:
|
|||||||
|
|
||||||
|
|
||||||
def route_board(
|
def route_board(
|
||||||
board: BasicBoard, *, trace_width: int, clearance: int, layer: int = 0
|
board: BasicBoard,
|
||||||
|
*,
|
||||||
|
trace_width: int,
|
||||||
|
clearance: int,
|
||||||
|
layers: list[int] | None = None,
|
||||||
|
via_cost: float = 10.0,
|
||||||
) -> RouteResult:
|
) -> RouteResult:
|
||||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||||
router = GridRouter(board, trace_width=trace_width, clearance=clearance, layer=layer)
|
router = GridRouter(
|
||||||
|
board,
|
||||||
|
trace_width=trace_width,
|
||||||
|
clearance=clearance,
|
||||||
|
layers=layers,
|
||||||
|
via_cost=via_cost,
|
||||||
|
)
|
||||||
return router.route()
|
return router.route()
|
||||||
|
|||||||
@ -5,15 +5,16 @@ writer — the Java-free replacement for the ``freerouting.jar`` step:
|
|||||||
|
|
||||||
dsn_text -> parse_dsn -> build_board -> route_board -> write_ses -> ses_text
|
dsn_text -> parse_dsn -> build_board -> route_board -> write_ses -> ses_text
|
||||||
|
|
||||||
Router output is in board units; it is converted back to DSN units (dividing by
|
Router output is in board units; wire paths and via locations are converted back
|
||||||
the resolution) for the SES ``(wire (path ...))`` scopes.
|
to DSN units (dividing by the resolution) for the SES ``(wire (path ...))`` and
|
||||||
|
``(via <padstack> x y)`` scopes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from freeroute.board import build_board
|
from freeroute.board import build_board
|
||||||
from freeroute.dsn import DsnBoard, parse_dsn
|
from freeroute.dsn import DsnBoard, parse_dsn
|
||||||
from freeroute.ses import RoutedWire, RoutingResult, write_ses
|
from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
||||||
|
|
||||||
from .grid_router import RouteResult, route_board
|
from .grid_router import RouteResult, route_board
|
||||||
|
|
||||||
@ -22,6 +23,8 @@ __all__ = ["route", "route_dsn_board", "build_routing_result"]
|
|||||||
#: fallback trace width / clearance in DSN units when the DSN has no rules
|
#: fallback trace width / clearance in DSN units when the DSN has no rules
|
||||||
_DEFAULT_WIDTH_DSN = 2000
|
_DEFAULT_WIDTH_DSN = 2000
|
||||||
_DEFAULT_CLEARANCE_DSN = 2000
|
_DEFAULT_CLEARANCE_DSN = 2000
|
||||||
|
#: fallback via padstack name when the DSN declares none
|
||||||
|
_DEFAULT_VIA = "Via"
|
||||||
|
|
||||||
|
|
||||||
def _rule_width_dsn(dsn: DsnBoard) -> float:
|
def _rule_width_dsn(dsn: DsnBoard) -> float:
|
||||||
@ -36,41 +39,53 @@ def _rule_clearance_dsn(dsn: DsnBoard) -> float:
|
|||||||
return _DEFAULT_CLEARANCE_DSN
|
return _DEFAULT_CLEARANCE_DSN
|
||||||
|
|
||||||
|
|
||||||
def route_dsn_board(dsn: DsnBoard) -> tuple[RouteResult, int, list[str]]:
|
def _via_padstack(dsn: DsnBoard) -> str:
|
||||||
|
return dsn.via_padstack_names[0] if dsn.via_padstack_names else _DEFAULT_VIA
|
||||||
|
|
||||||
|
|
||||||
|
def route_dsn_board(
|
||||||
|
dsn: DsnBoard, *, layers: list[int] | None = None
|
||||||
|
) -> 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."""
|
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||||
|
indices (default: all signal layers, i.e. multi-layer with vias)."""
|
||||||
board = build_board(dsn)
|
board = build_board(dsn)
|
||||||
scale = max(dsn.resolution.value, 1)
|
scale = max(dsn.resolution.value, 1)
|
||||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
result = route_board(board, trace_width=width_board, clearance=clearance_board, layer=0)
|
result = route_board(board, trace_width=width_board, clearance=clearance_board, layers=layers)
|
||||||
return result, scale, [layer.name for layer in dsn.layers]
|
return result, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
|
|
||||||
def build_routing_result(dsn: DsnBoard) -> RoutingResult:
|
def build_routing_result(dsn: DsnBoard, *, layers: list[int] | None = None) -> RoutingResult:
|
||||||
"""Route ``dsn`` and convert the board-unit paths to a DSN-unit
|
"""Route ``dsn`` and convert the board-unit paths + vias to a DSN-unit
|
||||||
:class:`~freeroute.ses.RoutingResult` for SES emission."""
|
:class:`~freeroute.ses.RoutingResult` for SES emission."""
|
||||||
route, scale, layer_names = route_dsn_board(dsn)
|
route, scale, layer_names = route_dsn_board(dsn, layers=layers)
|
||||||
layer_name = layer_names[route.layer] if layer_names else "F.Cu"
|
|
||||||
width_dsn = _rule_width_dsn(dsn)
|
width_dsn = _rule_width_dsn(dsn)
|
||||||
|
via_name = _via_padstack(dsn)
|
||||||
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
||||||
numbers_to_names = {i + 1: n.name for i, n in enumerate(dsn.nets)}
|
numbers_to_names = {i + 1: n.name for i, n in enumerate(dsn.nets)}
|
||||||
|
|
||||||
result = RoutingResult()
|
result = RoutingResult()
|
||||||
for net_no, paths in route.wires.items():
|
for net_no, segments in route.wires.items():
|
||||||
name = numbers_to_names.get(net_no, str(net_no))
|
name = numbers_to_names.get(net_no, str(net_no))
|
||||||
for path in paths:
|
for layer_index, path in segments:
|
||||||
|
layer_name = layer_names[layer_index] if layer_index < len(layer_names) else "F.Cu"
|
||||||
coords: list[float] = []
|
coords: list[float] = []
|
||||||
for point in path:
|
for point in path:
|
||||||
coords.append(point.x / scale)
|
coords.append(point.x / scale)
|
||||||
coords.append(point.y / scale)
|
coords.append(point.y / scale)
|
||||||
if len(coords) >= 4:
|
if len(coords) >= 4:
|
||||||
result.add_wire(name, RoutedWire(layer=layer_name, width=width_dsn, coords=coords))
|
result.add_wire(name, RoutedWire(layer=layer_name, width=width_dsn, coords=coords))
|
||||||
|
for net_no, locations in route.vias.items():
|
||||||
|
name = numbers_to_names.get(net_no, str(net_no))
|
||||||
|
for loc in locations:
|
||||||
|
result.add_via(name, RoutedVia(padstack=via_name, x=loc.x / scale, y=loc.y / scale))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def route(dsn_text: str) -> str:
|
def route(dsn_text: str, *, layers: list[int] | None = None) -> str:
|
||||||
"""Route a Specctra DSN string and return the routed SES string."""
|
"""Route a Specctra DSN string and return the routed SES string."""
|
||||||
dsn = parse_dsn(dsn_text)
|
dsn = parse_dsn(dsn_text)
|
||||||
result = build_routing_result(dsn)
|
result = build_routing_result(dsn, layers=layers)
|
||||||
return write_ses(dsn, result)
|
return write_ses(dsn, result)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user