Route to the real board outline, per connection, and out of fine-pitch pins
- The exact engine verified copper per net and dropped a whole net on any conflict: a 25-pin +3V3 net lost every connection to one bad segment. Verify and accept per ratsnest connection instead. - Pad and keepout cells were rounded outward, blocking cells up to half a step clear of the copper; 62 of the 64 pins of a 0.5 mm pitch LQFP could not start a route. Block only cells whose centre is inside the clearance zone, and let a net walk along inside its own pad, which adds no copper. - Routing was bounded by the outline's bounding box, so tracks cut across rounded corners. Keep the DSN boundary as a polygon; the grid blocks cells too close to it (scanline plus per-edge, precomputed for vias) and the exact and room engines reject copper that is. - --edge-clearance MM sets copper-to-edge clearance, e.g. KiCad's min_copper_edge_clearance, which the DSN does not carry. Default 0. On the TerraHawk rc4 power stage the exact engine now matches the FreeRouting JAR: same open count, no DRC errors, with a 0.5 mm edge rule.
This commit is contained in:
@@ -22,7 +22,7 @@ __all__ = ["BasicBoard"]
|
||||
class BasicBoard:
|
||||
"""A board: layers, nets, clearances, a bounding box, and items."""
|
||||
|
||||
__slots__ = ("layer_structure", "nets", "clearance_matrix", "bounding_box", "_items")
|
||||
__slots__ = ("layer_structure", "nets", "clearance_matrix", "bounding_box", "outline", "_items")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -35,6 +35,8 @@ class BasicBoard:
|
||||
self.nets = nets
|
||||
self.clearance_matrix = clearance_matrix
|
||||
self.bounding_box = bounding_box
|
||||
#: the real board outline, when the DSN has a polygon boundary
|
||||
self.outline = None
|
||||
self._items: list[Item] = []
|
||||
|
||||
# --- layers -------------------------------------------------------------
|
||||
|
||||
@@ -50,6 +50,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
||||
bounding_box = _build_bounding_box(dsn, transform)
|
||||
|
||||
board = BasicBoard(layer_structure, nets, clearance, bounding_box)
|
||||
board.outline = _build_outline(dsn, transform)
|
||||
|
||||
_insert_pins(dsn, board, transform, terminal_to_net)
|
||||
_insert_keepouts(dsn, board, transform)
|
||||
@@ -99,6 +100,18 @@ def _build_bounding_box(dsn: DsnBoard, transform: CoordinateTransform) -> IntBox
|
||||
return IntBox(llx, lly, urx, ury)
|
||||
|
||||
|
||||
def _build_outline(dsn: DsnBoard, transform: CoordinateTransform):
|
||||
"""The DSN boundary as a polygon, or None when it is not a polygon."""
|
||||
from .outline import BoardOutline
|
||||
|
||||
shape = dsn.boundary
|
||||
if not isinstance(shape, (Polygon, Path)):
|
||||
return None
|
||||
c = shape.coords
|
||||
pts = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
||||
return BoardOutline(pts) if len(pts) >= 3 else None
|
||||
|
||||
|
||||
def _shape_coords(shape: Shape) -> list[float]:
|
||||
if isinstance(shape, Rectangle):
|
||||
return list(shape.coords)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""The board outline as a polygon, and clearance tests against its edge.
|
||||
|
||||
The DSN boundary is the real Edge.Cuts outline, rounded corners and all
|
||||
(arcs come as short segments). Routing inside its bounding box instead lets
|
||||
copper cut across a rounded corner closer to the edge than the board allows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
|
||||
|
||||
class BoardOutline:
|
||||
"""A simple polygon in board units, with point and box clearance tests."""
|
||||
|
||||
def __init__(self, points: list[IntPoint]) -> None:
|
||||
pts = list(points)
|
||||
if len(pts) > 1 and pts[0] == pts[-1]:
|
||||
pts.pop()
|
||||
self.points = pts
|
||||
self._edges = [(pts[i], pts[(i + 1) % len(pts)]) for i in range(len(pts))]
|
||||
|
||||
def contains(self, p: IntPoint) -> bool:
|
||||
"""Even-odd point in polygon; points on the edge count as outside."""
|
||||
inside = False
|
||||
x, y = p.x, p.y
|
||||
for a, b in self._edges:
|
||||
if (a.y > y) != (b.y > y):
|
||||
t = (y - a.y) / (b.y - a.y)
|
||||
if x < a.x + t * (b.x - a.x):
|
||||
inside = not inside
|
||||
return inside
|
||||
|
||||
def row_inside_spans(self, y: float) -> list[tuple[float, float]]:
|
||||
"""The x intervals inside the polygon along the horizontal line at ``y``."""
|
||||
xs = sorted(
|
||||
a.x + (y - a.y) * (b.x - a.x) / (b.y - a.y)
|
||||
for a, b in self._edges
|
||||
if (a.y > y) != (b.y > y)
|
||||
)
|
||||
# an odd count (a line grazing a vertex) drops the unpaired crossing
|
||||
return list(zip(xs[0::2], xs[1::2], strict=False))
|
||||
|
||||
def edges(self):
|
||||
return self._edges
|
||||
|
||||
def edge_distance(self, p: IntPoint) -> float:
|
||||
return min(_point_segment(p, a, b) for a, b in self._edges)
|
||||
|
||||
def point_clear(self, p: IntPoint, reach: float) -> bool:
|
||||
"""Copper reaching ``reach`` around ``p`` stays inside, clear of the edge."""
|
||||
return self.contains(p) and self.edge_distance(p) >= reach
|
||||
|
||||
def box_clear(self, box: IntBox, clearance: float) -> bool:
|
||||
"""``box`` lies inside the outline at least ``clearance`` from its edge."""
|
||||
corners = [
|
||||
IntPoint(box.ll.x, box.ll.y),
|
||||
IntPoint(box.ur.x, box.ll.y),
|
||||
IntPoint(box.ur.x, box.ur.y),
|
||||
IntPoint(box.ll.x, box.ur.y),
|
||||
]
|
||||
if not all(self.contains(c) for c in corners):
|
||||
return False
|
||||
return all(_segment_box(a, b, box) >= clearance for a, b in self._edges)
|
||||
|
||||
|
||||
def _point_segment(p: IntPoint, a: IntPoint, b: IntPoint) -> float:
|
||||
dx, dy = b.x - a.x, b.y - a.y
|
||||
length2 = dx * dx + dy * dy
|
||||
if length2 == 0:
|
||||
return math.hypot(p.x - a.x, p.y - a.y)
|
||||
t = max(0.0, min(1.0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / length2))
|
||||
return math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy))
|
||||
|
||||
|
||||
def _point_box(p: IntPoint, box: IntBox) -> float:
|
||||
dx = max(box.ll.x - p.x, 0, p.x - box.ur.x)
|
||||
dy = max(box.ll.y - p.y, 0, p.y - box.ur.y)
|
||||
return math.hypot(dx, dy)
|
||||
|
||||
|
||||
def _segment_box(a: IntPoint, b: IntPoint, box: IntBox) -> float:
|
||||
"""Distance between segment ``ab`` and an axis-aligned box (0 if they touch)."""
|
||||
if _segments_cross_box(a, b, box):
|
||||
return 0.0
|
||||
corners = [
|
||||
IntPoint(box.ll.x, box.ll.y),
|
||||
IntPoint(box.ur.x, box.ll.y),
|
||||
IntPoint(box.ur.x, box.ur.y),
|
||||
IntPoint(box.ll.x, box.ur.y),
|
||||
]
|
||||
return min(
|
||||
_point_box(a, box),
|
||||
_point_box(b, box),
|
||||
*(_point_segment(c, a, b) for c in corners),
|
||||
)
|
||||
|
||||
|
||||
def _segments_cross_box(a: IntPoint, b: IntPoint, box: IntBox) -> bool:
|
||||
"""Liang-Barsky clip: does segment ``ab`` enter the box?"""
|
||||
t0, t1 = 0.0, 1.0
|
||||
dx, dy = b.x - a.x, b.y - a.y
|
||||
for p, q in (
|
||||
(-dx, a.x - box.ll.x),
|
||||
(dx, box.ur.x - a.x),
|
||||
(-dy, a.y - box.ll.y),
|
||||
(dy, box.ur.y - a.y),
|
||||
):
|
||||
if p == 0:
|
||||
if q < 0:
|
||||
return False
|
||||
else:
|
||||
r = q / p
|
||||
if p < 0:
|
||||
t0 = max(t0, r)
|
||||
else:
|
||||
t1 = min(t1, r)
|
||||
if t0 > t1:
|
||||
return False
|
||||
return True
|
||||
+16
-2
@@ -110,6 +110,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
help="disable rip-up-and-retry (grid and exact engines)",
|
||||
)
|
||||
parser.add_argument("--layers", help="comma-separated signal layer indices to route on")
|
||||
parser.add_argument(
|
||||
"--edge-clearance",
|
||||
type=float,
|
||||
metavar="MM",
|
||||
help="copper to board-edge clearance in mm, e.g. KiCad's min_copper_edge_clearance "
|
||||
"(the DSN does not carry it); default 0: copper only has to stay inside the outline",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -158,11 +165,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
diagonal=args.diagonal,
|
||||
rip_up=rip_up,
|
||||
max_passes=max_passes,
|
||||
edge_clearance_mm=args.edge_clearance,
|
||||
)
|
||||
elif args.engine == "room":
|
||||
result = build_rooms_routing_result(dsn, layers=layers, shove=args.shove, pack=args.pack)
|
||||
result = build_rooms_routing_result(
|
||||
dsn, layers=layers, shove=args.shove, pack=args.pack,
|
||||
edge_clearance_mm=args.edge_clearance,
|
||||
)
|
||||
else:
|
||||
result = build_routing_result(dsn, layers=layers, rip_up=rip_up, max_passes=max_passes)
|
||||
result = build_routing_result(
|
||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
||||
edge_clearance_mm=args.edge_clearance,
|
||||
)
|
||||
|
||||
ses_text = write_ses(dsn, result)
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ def route_board_exact(
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
via_half: int | None = None,
|
||||
edge_clearance: int | None = None,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
@@ -116,7 +117,10 @@ def route_board_exact(
|
||||
rip_up=rip_up,
|
||||
orthogonal=True,
|
||||
via_half=via_half,
|
||||
edge_clearance=edge_clearance,
|
||||
)
|
||||
edge = 0 if edge_clearance is None else edge_clearance
|
||||
outline = board.outline
|
||||
|
||||
tree = ShapeSearchTree()
|
||||
owner = _counter()
|
||||
@@ -143,20 +147,25 @@ def route_board_exact(
|
||||
via_layers = frozenset(signal_layers)
|
||||
accepted: dict[int, _Accepted] = {}
|
||||
|
||||
for net_no in sorted(set(result.wires) | set(result.vias)):
|
||||
boxes = _net_boxes(result, net_no, half_width, via_layers, via_half)
|
||||
# Verify connection by connection, so one conflicting segment of a
|
||||
# 25-pin supply net costs that one connection, not the whole net.
|
||||
for net_no, wires, vias in sorted(result.connections, key=lambda c: c[0]):
|
||||
boxes = _copper_boxes(wires, vias, half_width, via_layers, via_half)
|
||||
if any(
|
||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||
for box, box_layers in boxes
|
||||
for layer in box_layers
|
||||
):
|
||||
) or (outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)):
|
||||
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||
acc = _Accepted(wires=result.wires.get(net_no, []), vias=result.vias.get(net_no, []))
|
||||
acc = accepted.get(net_no)
|
||||
if acc is None:
|
||||
acc = accepted[net_no] = _Accepted(wires=[], vias=[])
|
||||
acc.wires.extend(wires)
|
||||
acc.vias.extend(vias)
|
||||
for box, box_layers in boxes:
|
||||
oid = next(owner)
|
||||
tree.insert(TreeShape(oid, net_no, box_layers, box, routed=True))
|
||||
acc.owners.append(oid)
|
||||
accepted[net_no] = acc
|
||||
|
||||
# a net is dropped if it has a real ratsnest (>= 2 pins on a signal layer)
|
||||
# but was not accepted — whether the grid failed to route it or its exact
|
||||
@@ -547,12 +556,18 @@ def _rollback(shoved, accepted, tree, owner) -> int: # noqa: ARG001
|
||||
|
||||
|
||||
def _net_boxes(result, net_no, half_width, via_layers, via_half=None):
|
||||
return _copper_boxes(
|
||||
result.wires.get(net_no, []), result.vias.get(net_no, []), half_width, via_layers, via_half
|
||||
)
|
||||
|
||||
|
||||
def _copper_boxes(wires, vias, half_width, via_layers, via_half=None):
|
||||
via_half = via_half or half_width
|
||||
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
||||
for layer, points in result.wires.get(net_no, []):
|
||||
for layer, points in wires:
|
||||
for box in PolylineShape(Polyline(points), half_width).tiles():
|
||||
boxes.append((box, frozenset({layer})))
|
||||
for loc in result.vias.get(net_no, []):
|
||||
for loc in vias:
|
||||
vbox = IntBox(loc.x - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half)
|
||||
boxes.append((vbox, via_layers))
|
||||
return boxes
|
||||
|
||||
@@ -31,7 +31,7 @@ import math
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ObstacleArea, Pin
|
||||
from freeroute.geometry import IntPoint
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
|
||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||
|
||||
@@ -59,7 +59,7 @@ class RouteResult:
|
||||
``routed_net_numbers`` are the nets that got any wire or via.
|
||||
"""
|
||||
|
||||
__slots__ = ("wires", "vias", "half_width", "unrouted")
|
||||
__slots__ = ("wires", "vias", "half_width", "unrouted", "connections")
|
||||
|
||||
def __init__(self, half_width: int) -> None:
|
||||
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
|
||||
@@ -67,6 +67,8 @@ class RouteResult:
|
||||
self.half_width = half_width
|
||||
#: number of ratsnest connections left unrouted
|
||||
self.unrouted = 0
|
||||
#: the same copper per routed connection: (net_no, wires, vias)
|
||||
self.connections: list[tuple[int, list[tuple[int, list[IntPoint]]], list[IntPoint]]] = []
|
||||
|
||||
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
|
||||
self.wires.setdefault(net_no, []).append((layer, path))
|
||||
@@ -132,6 +134,7 @@ class GridRouter:
|
||||
rip_penalty: float = 30.0,
|
||||
orthogonal: bool = False,
|
||||
via_half: int | None = None,
|
||||
edge_clearance: int | None = None,
|
||||
) -> None:
|
||||
self.board = board
|
||||
self.trace_width = max(trace_width, 1)
|
||||
@@ -151,6 +154,8 @@ class GridRouter:
|
||||
# centre via_half + clearance + half_width from the via's centre, so a
|
||||
# via also claims every cell within via_reach of its own.
|
||||
self.via_half = via_half or self.half_width
|
||||
# copper to board edge; KiCad's rule is usually wider than clearance
|
||||
self.edge_clearance = 0 if edge_clearance is None else edge_clearance
|
||||
need = self.via_half + self.clearance + self.half_width
|
||||
self.via_reach = max(0, math.ceil(need / self.step) - 1)
|
||||
|
||||
@@ -166,6 +171,7 @@ class GridRouter:
|
||||
|
||||
# static obstacles
|
||||
self._pad_block: dict[Node, set[int]] = {}
|
||||
self._pad_own: dict[Node, set[int]] = {}
|
||||
self._keepout: set[Node] = set()
|
||||
# per-connection occupancy (owner = connection index)
|
||||
self._cell_owner: dict[Node, int] = {}
|
||||
@@ -185,16 +191,57 @@ class GridRouter:
|
||||
return IntPoint(self.origin_x + cell[0] * self.step, self.origin_y + cell[1] * self.step)
|
||||
|
||||
def _cells_in_box(self, box, margin: int):
|
||||
lo_x = round((box.ll.x - margin - self.origin_x) / self.step)
|
||||
hi_x = round((box.ur.x + margin - self.origin_x) / self.step)
|
||||
lo_y = round((box.ll.y - margin - self.origin_y) / self.step)
|
||||
hi_y = round((box.ur.y + margin - self.origin_y) / self.step)
|
||||
"""Cells whose centre lies strictly inside ``box`` grown by ``margin``.
|
||||
|
||||
A trace centred there would come closer than ``margin`` to the box.
|
||||
Rounding the bounds outward instead blocked cells up to half a step
|
||||
clear of it, which walled in every pin of a 0.5 mm pitch package.
|
||||
"""
|
||||
lo_x = math.floor((box.ll.x - margin - self.origin_x) / self.step) + 1
|
||||
hi_x = math.ceil((box.ur.x + margin - self.origin_x) / self.step) - 1
|
||||
lo_y = math.floor((box.ll.y - margin - self.origin_y) / self.step) + 1
|
||||
hi_y = math.ceil((box.ur.y + margin - self.origin_y) / self.step) - 1
|
||||
for gx in range(max(lo_x, 0), min(hi_x, self.cols - 1) + 1):
|
||||
for gy in range(max(lo_y, 0), min(hi_y, self.rows - 1) + 1):
|
||||
yield gx, gy
|
||||
|
||||
def _outline_blocked(self, reach: int) -> set[Cell]:
|
||||
"""Cells whose centre is outside the outline or within ``reach`` of its edge."""
|
||||
outline = self.board.outline
|
||||
blocked: set[Cell] = set()
|
||||
for gy in range(self.rows): # outside: one scanline per row
|
||||
y = self.origin_y + gy * self.step
|
||||
spans = outline.row_inside_spans(y)
|
||||
for gx in range(self.cols):
|
||||
x = self.origin_x + gx * self.step
|
||||
if not any(lo < x < hi for lo, hi in spans):
|
||||
blocked.add((gx, gy))
|
||||
for a, b in outline.edges(): # too close: only cells near each edge
|
||||
edge_box = IntBox(min(a.x, b.x), min(a.y, b.y), max(a.x, b.x), max(a.y, b.y))
|
||||
for cell in self._cells_in_box(edge_box, reach):
|
||||
if cell not in blocked and outline.edge_distance(self._cell_center(cell)) < reach:
|
||||
blocked.add(cell)
|
||||
return blocked
|
||||
|
||||
def _inside_own_pad(self, cell: Cell, shape) -> bool:
|
||||
"""A trace centred on ``cell`` lies entirely on the pad's own copper."""
|
||||
c = self._cell_center(cell)
|
||||
h = self.half_width
|
||||
return all(
|
||||
shape.contains(IntPoint(c.x + dx, c.y + dy)) for dx in (-h, h) for dy in (-h, h)
|
||||
)
|
||||
|
||||
def _rasterize_obstacles(self) -> None:
|
||||
margin = self.clearance + self.half_width
|
||||
outline = self.board.outline
|
||||
self._via_edge_block: set[Cell] = set()
|
||||
if outline is not None:
|
||||
# cells too close to the real outline (rounded corners included):
|
||||
# for a trace on every layer, and separately for a via's wider copper
|
||||
for cell in self._outline_blocked(self.half_width + self.edge_clearance):
|
||||
for layer in self.layers:
|
||||
self._keepout.add((*cell, layer))
|
||||
self._via_edge_block = self._outline_blocked(self.via_half + self.edge_clearance)
|
||||
for item in self.board.get_items():
|
||||
if isinstance(item, Pin):
|
||||
box = item.shape.bounding_box() if item.shape is not None else None
|
||||
@@ -205,6 +252,13 @@ class GridRouter:
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
for layer in pin_layers:
|
||||
self._pad_block.setdefault((*cell, layer), set()).add(net)
|
||||
# Copper of the pin's own net: a trace inside it adds nothing,
|
||||
# so it is open to that net however close the neighbours are.
|
||||
# That is how a route leaves a fine-pitch pin.
|
||||
for cell in self._cells_in_box(box, 0):
|
||||
if self._inside_own_pad(cell, item.shape):
|
||||
for layer in pin_layers:
|
||||
self._pad_own.setdefault((*cell, layer), set()).add(net)
|
||||
elif isinstance(item, ObstacleArea):
|
||||
if item.layer not in self.layers:
|
||||
continue
|
||||
@@ -214,7 +268,8 @@ class GridRouter:
|
||||
box = tile.bounding_box()
|
||||
if box.is_empty():
|
||||
continue
|
||||
for cell in self._cells_in_box(box, self.clearance):
|
||||
# the same distance the exact engine verifies against
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
self._keepout.add((*cell, item.layer))
|
||||
|
||||
# --- occupancy cost -----------------------------------------------------
|
||||
@@ -233,7 +288,7 @@ class GridRouter:
|
||||
if node in self._keepout:
|
||||
return None
|
||||
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) and net_no not in self._pad_own.get(node, ()):
|
||||
return None
|
||||
blocking = None
|
||||
owner = self._cell_owner.get(node)
|
||||
@@ -264,6 +319,8 @@ class GridRouter:
|
||||
|
||||
def _via_placeable(self, cell: Cell, net_no: int, conns: list[_Connection]) -> bool:
|
||||
"""A through via needs its whole footprint free of other nets on every layer."""
|
||||
if cell in self._via_edge_block:
|
||||
return False
|
||||
return all(
|
||||
self._enter_cost(c, layer, net_no, False, conns) == 0.0
|
||||
for c in self._via_cells(cell)
|
||||
@@ -464,6 +521,7 @@ class GridRouter:
|
||||
result.add_wire(net_no, layer, points)
|
||||
for location in vias:
|
||||
result.add_via(net_no, location)
|
||||
result.connections.append((net_no, wires, vias))
|
||||
return result
|
||||
|
||||
|
||||
@@ -520,6 +578,7 @@ def route_board(
|
||||
max_passes: int = 10,
|
||||
orthogonal: bool = False,
|
||||
via_half: int | None = None,
|
||||
edge_clearance: int | None = None,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -532,5 +591,6 @@ def route_board(
|
||||
max_passes=max_passes,
|
||||
orthogonal=orthogonal,
|
||||
via_half=via_half,
|
||||
edge_clearance=edge_clearance,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
@@ -45,6 +45,14 @@ def _via_padstack(dsn: DsnBoard) -> str:
|
||||
return dsn.via_padstack_names[0] if dsn.via_padstack_names else _DEFAULT_VIA
|
||||
|
||||
|
||||
def _mm_to_board(dsn: DsnBoard, scale: int, value_mm: float | None) -> int | None:
|
||||
"""A length in millimetres as board units, or None."""
|
||||
if value_mm is None:
|
||||
return None
|
||||
unit_mm = {"inch": 25.4, "mil": 0.0254, "mm": 1.0, "cm": 10.0, "um": 0.001}[dsn.unit]
|
||||
return round(value_mm / unit_mm * scale)
|
||||
|
||||
|
||||
def _via_half_board(dsn: DsnBoard, scale: int) -> int | None:
|
||||
"""Half the via pad's size in board units, from the DSN's via padstack.
|
||||
|
||||
@@ -67,6 +75,7 @@ def _via_half_board(dsn: DsnBoard, scale: int) -> int | None:
|
||||
def route_dsn_board(
|
||||
dsn: DsnBoard,
|
||||
*,
|
||||
edge_clearance_mm: float | None = None,
|
||||
layers: list[int] | None = None,
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
@@ -77,11 +86,13 @@ def route_dsn_board(
|
||||
toggles rip-up-and-retry (``False`` = greedy single pass)."""
|
||||
board = build_board(dsn)
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
edge_clearance_board = _mm_to_board(dsn, scale, edge_clearance_mm)
|
||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
result = route_board(
|
||||
board,
|
||||
via_half=_via_half_board(dsn, scale),
|
||||
edge_clearance=edge_clearance_board,
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
@@ -122,10 +133,12 @@ def build_routing_result(
|
||||
layers: list[int] | None = None,
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
edge_clearance_mm: float | None = None,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||
route, scale, layer_names = route_dsn_board(
|
||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes
|
||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
||||
edge_clearance_mm=edge_clearance_mm,
|
||||
)
|
||||
return _to_routing_result(route, dsn, scale, layer_names)
|
||||
|
||||
@@ -133,6 +146,7 @@ def build_routing_result(
|
||||
def route_dsn_board_exact(
|
||||
dsn: DsnBoard,
|
||||
*,
|
||||
edge_clearance_mm: float | None = None,
|
||||
layers: list[int] | None = None,
|
||||
max_passes: int = 10,
|
||||
rip_up: bool = True,
|
||||
@@ -146,11 +160,13 @@ def route_dsn_board_exact(
|
||||
the 45-degree recovery pass for still-dropped 2-pin nets."""
|
||||
board = build_board(dsn)
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
edge_clearance_board = _mm_to_board(dsn, scale, edge_clearance_mm)
|
||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
exact = route_board_exact(
|
||||
board,
|
||||
via_half=_via_half_board(dsn, scale),
|
||||
edge_clearance=edge_clearance_board,
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
@@ -170,6 +186,7 @@ def build_exact_routing_result(
|
||||
diagonal: bool = False,
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
edge_clearance_mm: float | None = None,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(
|
||||
@@ -179,12 +196,18 @@ def build_exact_routing_result(
|
||||
diagonal=diagonal,
|
||||
rip_up=rip_up,
|
||||
max_passes=max_passes,
|
||||
edge_clearance_mm=edge_clearance_mm,
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
def route_dsn_board_rooms(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False, pack: bool = False
|
||||
dsn: DsnBoard,
|
||||
*,
|
||||
layers: list[int] | None = None,
|
||||
shove: bool = False,
|
||||
pack: bool = False,
|
||||
edge_clearance_mm: float | None = None,
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the continuous expansion-room track;
|
||||
return the result, the scale, and layer names. ``shove`` enables occupancy-
|
||||
@@ -192,11 +215,13 @@ def route_dsn_board_rooms(
|
||||
``pack`` enables the coordinated multi-trace channel packer."""
|
||||
board = build_board(dsn)
|
||||
scale = max(dsn.resolution.value, 1)
|
||||
edge_clearance_board = _mm_to_board(dsn, scale, edge_clearance_mm)
|
||||
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
rooms = route_board_rooms(
|
||||
board,
|
||||
via_half=_via_half_board(dsn, scale),
|
||||
edge_clearance=edge_clearance_board,
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
@@ -207,10 +232,17 @@ def route_dsn_board_rooms(
|
||||
|
||||
|
||||
def build_rooms_routing_result(
|
||||
dsn: DsnBoard, *, layers: list[int] | None = None, shove: bool = False, pack: bool = False
|
||||
dsn: DsnBoard,
|
||||
*,
|
||||
layers: list[int] | None = None,
|
||||
shove: bool = False,
|
||||
pack: bool = False,
|
||||
edge_clearance_mm: float | None = None,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (room track) and convert to a DSN-unit RoutingResult."""
|
||||
rooms, scale, layer_names = route_dsn_board_rooms(dsn, layers=layers, shove=shove, pack=pack)
|
||||
rooms, scale, layer_names = route_dsn_board_rooms(
|
||||
dsn, layers=layers, shove=shove, pack=pack, edge_clearance_mm=edge_clearance_mm
|
||||
)
|
||||
return _to_routing_result(rooms.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
@@ -222,6 +254,7 @@ def route(
|
||||
shove: bool = False,
|
||||
pack: bool = False,
|
||||
diagonal: bool = False,
|
||||
edge_clearance_mm: float | None = None,
|
||||
) -> str:
|
||||
"""Route a Specctra DSN string and return the routed SES string.
|
||||
|
||||
@@ -235,9 +268,13 @@ def route(
|
||||
"""
|
||||
dsn = parse_dsn(dsn_text)
|
||||
if engine == "exact":
|
||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove, diagonal=diagonal)
|
||||
result = build_exact_routing_result(
|
||||
dsn, layers=layers, shove=shove, diagonal=diagonal, edge_clearance_mm=edge_clearance_mm
|
||||
)
|
||||
elif engine == "room":
|
||||
result = build_rooms_routing_result(dsn, layers=layers, shove=shove, pack=pack)
|
||||
result = build_rooms_routing_result(
|
||||
dsn, layers=layers, shove=shove, pack=pack, edge_clearance_mm=edge_clearance_mm
|
||||
)
|
||||
else:
|
||||
result = build_routing_result(dsn, layers=layers)
|
||||
result = build_routing_result(dsn, layers=layers, edge_clearance_mm=edge_clearance_mm)
|
||||
return write_ses(dsn, result)
|
||||
|
||||
@@ -472,6 +472,7 @@ def route_board_rooms(
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
via_half: int | None = None,
|
||||
edge_clearance: int | None = None,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` with the continuous expansion-room track.
|
||||
|
||||
@@ -498,6 +499,7 @@ def route_board_rooms(
|
||||
# half the via pad's size: a via is not a trace, and sizing it like one
|
||||
# let routes pass within a via's real copper
|
||||
via_half = via_half or half_width
|
||||
edge = (board.outline, 0 if edge_clearance is None else edge_clearance)
|
||||
margin = clearance + half_width
|
||||
outline = board.bounding_box
|
||||
|
||||
@@ -541,7 +543,7 @@ def route_board_rooms(
|
||||
via_layers,
|
||||
accepted,
|
||||
shove,
|
||||
via_half=via_half)
|
||||
via_half=via_half, edge=edge)
|
||||
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||
if net_no in packed:
|
||||
continue
|
||||
@@ -566,7 +568,7 @@ def route_board_rooms(
|
||||
accepted,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
via_half=via_half)
|
||||
via_half=via_half, edge=edge)
|
||||
|
||||
drc_clean = tree.has_violation(clearance) is None
|
||||
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
||||
@@ -592,6 +594,7 @@ def _route_connection(
|
||||
max_shove_depth=5,
|
||||
shove_cap=3,
|
||||
via_half=None,
|
||||
edge=None,
|
||||
):
|
||||
rooms_by_layer = {
|
||||
layer: _free_rooms(tree, outline, layer, net_no, margin, half_width)
|
||||
@@ -612,7 +615,7 @@ def _route_connection(
|
||||
|
||||
if realized is not None:
|
||||
wires, vias = realized
|
||||
boxes = _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=via_half)
|
||||
boxes = _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=via_half, edge=edge)
|
||||
if boxes is not None and not any(
|
||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||
for box, box_layers in boxes
|
||||
@@ -639,11 +642,11 @@ def _route_connection(
|
||||
clearance,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
via_half=via_half)
|
||||
via_half=via_half, edge=edge)
|
||||
return 0
|
||||
|
||||
|
||||
def _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=None):
|
||||
def _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=None, edge=None):
|
||||
via_half = via_half or half_width
|
||||
"""The ``(box, layers)`` copper tiles of a realized route, or ``None`` if any
|
||||
tile leaves the board outline (the same early-out as the old inline check)."""
|
||||
@@ -652,6 +655,8 @@ def _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=None):
|
||||
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
||||
if not outline.contains_box(box):
|
||||
return None
|
||||
if edge is not None and edge[0] is not None and not edge[0].box_clear(box, edge[1]):
|
||||
return None
|
||||
boxes.append((box, frozenset({layer})))
|
||||
for loc in vias:
|
||||
vbox = IntBox(
|
||||
@@ -864,6 +869,7 @@ def _pack_channels(
|
||||
accepted,
|
||||
shove,
|
||||
via_half=None,
|
||||
edge=None,
|
||||
) -> set[int]:
|
||||
"""Coordinated multi-trace channel packing (opt-in pre-pass).
|
||||
|
||||
@@ -932,7 +938,7 @@ def _pack_channels(
|
||||
spacing_min,
|
||||
via_layers,
|
||||
outline,
|
||||
via_half=via_half)
|
||||
via_half=via_half, edge=edge)
|
||||
if placed is None:
|
||||
continue # could not pack every member cleanly -> leave to greedy
|
||||
for net_no, wires in placed:
|
||||
@@ -969,6 +975,7 @@ def _pack_group(
|
||||
via_layers,
|
||||
outline,
|
||||
via_half=None,
|
||||
edge=None,
|
||||
):
|
||||
"""Assign parallel lanes + staggered fan-in trunks to every net crossing one
|
||||
gap, and return ``[(net_no, wires), ...]`` if ALL of them realize exactly
|
||||
@@ -1025,7 +1032,7 @@ def _pack_group(
|
||||
if len(corners) < 2:
|
||||
return None
|
||||
wires = [(layer, corners)]
|
||||
boxes = _collect_boxes(wires, [], half_width, via_layers, outline, via_half=via_half)
|
||||
boxes = _collect_boxes(wires, [], half_width, via_layers, outline, via_half=via_half, edge=edge)
|
||||
if boxes is None:
|
||||
return None
|
||||
if any(
|
||||
@@ -1061,6 +1068,7 @@ def _shove_recover_room(
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
via_half=None,
|
||||
edge=None,
|
||||
) -> int:
|
||||
"""Recover a dropped net by nudging the committed trace(s) that block it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user