Keep hole-to-hole spacing between every two vias, one net's included
Every pin of a supply drops its own via to the plane, and nothing kept two of them apart: the flight controller came back with 199 co-located holes and 109 hole-to-hole errors, nearly all VDD_3V3 and GND. A via now keeps its drill plus --hole-to-hole (default 0.25 mm, KiCad's) from every via centre, fixed ones included, except an own-net via at the same cell, which it reuses; such a via is written once, and none is written on top of a via already on the board.
This commit is contained in:
@@ -175,6 +175,14 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"connections a rip-up displaces are rerouted at once and the rip-up is undone "
|
||||
"if fewer connections end up routed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hole-to-hole",
|
||||
type=float,
|
||||
default=0.25,
|
||||
metavar="MM",
|
||||
help="grid and exact engines: least distance between the edges of any two via "
|
||||
"holes, of one net or two (KiCad's min_hole_to_hole; default 0.25)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jobs",
|
||||
type=int,
|
||||
@@ -239,6 +247,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"resolution": 2 if args.grid_resolution is None else args.grid_resolution,
|
||||
"heuristic_weight": args.heuristic_weight,
|
||||
"access_cost": args.access_cost or 0.0,
|
||||
"hole_to_hole_mm": args.hole_to_hole,
|
||||
"rollback": args.rollback,
|
||||
}
|
||||
elif args.engine == "grid":
|
||||
@@ -249,6 +258,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"edge_clearance_mm": args.edge_clearance,
|
||||
"resolution": 1 if args.grid_resolution is None else args.grid_resolution,
|
||||
"access_cost": args.access_cost or 0.0,
|
||||
"hole_to_hole_mm": args.hole_to_hole,
|
||||
"rollback": args.rollback,
|
||||
}
|
||||
if args.engine == "room":
|
||||
|
||||
@@ -100,6 +100,8 @@ def route_board_exact(
|
||||
access_cost: float = 0.0,
|
||||
rollback: bool = True,
|
||||
net_widths: dict[int, int] | None = None,
|
||||
via_drill: int | None = None,
|
||||
hole_to_hole: int = 0,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
@@ -133,6 +135,8 @@ def route_board_exact(
|
||||
access_cost=access_cost,
|
||||
rollback=rollback,
|
||||
net_widths=net_widths,
|
||||
via_drill=via_drill,
|
||||
hole_to_hole=hole_to_hole,
|
||||
)
|
||||
edge = 0 if edge_clearance is None else edge_clearance
|
||||
outline = board.outline
|
||||
|
||||
@@ -31,7 +31,7 @@ import math
|
||||
import random
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin
|
||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin, Via
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
from freeroute.geometry.polyline import corner_box
|
||||
|
||||
@@ -177,6 +177,8 @@ class GridRouter:
|
||||
access_cost: float = 0.0,
|
||||
rollback: bool = True,
|
||||
net_widths: dict[int, int] | None = None,
|
||||
via_drill: int | None = None,
|
||||
hole_to_hole: int = 0,
|
||||
) -> None:
|
||||
self.board = board
|
||||
# Pin access: while a pin still has connections to route, the cells
|
||||
@@ -248,6 +250,23 @@ class GridRouter:
|
||||
self._spread: list[tuple[int, int]] = [] # the current search's
|
||||
self._cur_half = self.half_width
|
||||
self._halo_by_half: dict[int, list[tuple[int, int]]] = {self.half_width: self._halo_offsets}
|
||||
# Hole-to-hole spacing holds between any two vias, of one net or two.
|
||||
# Other nets' vias are kept further apart already; a net's own vias
|
||||
# (every pin of a supply dropping to its plane) are not, so a via
|
||||
# must keep its drill plus the spacing from every via centre, except
|
||||
# an own-net via at the very same cell, which it simply reuses.
|
||||
drill = via_drill if via_drill else 2 * self.via_half
|
||||
need = drill + max(hole_to_hole, 0)
|
||||
reach = math.ceil(need / self.step)
|
||||
self._hole_offsets = [
|
||||
(dx, dy)
|
||||
for dx in range(-reach, reach + 1)
|
||||
for dy in range(-reach, reach + 1)
|
||||
if (dx or dy) and math.hypot(dx, dy) * self.step < need
|
||||
]
|
||||
self._via_nets: dict[Cell, dict[int, int]] = {} # routed via centres
|
||||
self._fixed_vias: dict[Cell, int] = {}
|
||||
self._fixed_via_points: set[tuple[int, IntPoint]] = set()
|
||||
|
||||
if layers is None:
|
||||
layers = [i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal]
|
||||
@@ -276,6 +295,10 @@ class GridRouter:
|
||||
self._halo: dict[Node, list[int]] = {}
|
||||
self._rasterize_obstacles()
|
||||
self._rasterize_fixed_copper()
|
||||
for item in board.get_items():
|
||||
if isinstance(item, Via) and item.fixed and item.net_nos:
|
||||
self._fixed_vias[self._cell_of(item.location)] = item.net_nos[0]
|
||||
self._fixed_via_points.add((item.net_nos[0], item.location))
|
||||
|
||||
# --- grid helpers -------------------------------------------------------
|
||||
|
||||
@@ -491,7 +514,21 @@ class GridRouter:
|
||||
return False
|
||||
if self._enter_cost_base(c, layer, net_no, False, conns, wire_keepout=False) != 0.0:
|
||||
return False
|
||||
return True
|
||||
return self._holes_clear(cell, net_no)
|
||||
|
||||
def _via_nets_at(self, cell: Cell) -> set[int]:
|
||||
nets = {net for net, n in self._via_nets.get(cell, {}).items() if n > 0}
|
||||
fixed = self._fixed_vias.get(cell)
|
||||
if fixed is not None:
|
||||
nets.add(fixed)
|
||||
return nets
|
||||
|
||||
def _holes_clear(self, cell: Cell, net_no: int) -> bool:
|
||||
"""No via centre within hole-to-hole reach, but an own-net one here."""
|
||||
if self._via_nets_at(cell) - {net_no}:
|
||||
return False
|
||||
cx, cy = cell
|
||||
return not any(self._via_nets_at((cx + dx, cy + dy)) for dx, dy in self._hole_offsets)
|
||||
|
||||
# --- A* search ----------------------------------------------------------
|
||||
|
||||
@@ -953,6 +990,9 @@ class GridRouter:
|
||||
via_cells.append((path[-1][0], path[-1][1]))
|
||||
conn.trace_cells = [(cell, layer) for layer, cells in wire_segs for cell in cells]
|
||||
conn.via_cells = list(via_cells)
|
||||
for cell in conn.via_cells:
|
||||
count = self._via_nets.setdefault(cell, {})
|
||||
count[conn.net_no] = count.get(conn.net_no, 0) + 1
|
||||
for cell, layer in conn.trace_cells:
|
||||
self._cell_owner[(*cell, layer)] = conn.index
|
||||
conn.half = half = self._conn_half(conn)
|
||||
@@ -1027,6 +1067,10 @@ class GridRouter:
|
||||
del self._halo[node]
|
||||
conn.halo_nodes = []
|
||||
conn.trace_cells = []
|
||||
for cell in conn.via_cells:
|
||||
count = self._via_nets.get(cell)
|
||||
if count and count.get(conn.net_no):
|
||||
count[conn.net_no] -= 1
|
||||
conn.via_cells = []
|
||||
conn.wires = []
|
||||
conn.vias = []
|
||||
@@ -1257,11 +1301,16 @@ class GridRouter:
|
||||
|
||||
def _result_from(self, snapshot, unrouted: int) -> RouteResult:
|
||||
result = RouteResult(self.half_width)
|
||||
written: set[tuple[int, IntPoint]] = set()
|
||||
result.unrouted = unrouted
|
||||
for net_no, wires, vias, ends in snapshot:
|
||||
for layer, points in wires:
|
||||
result.add_wire(net_no, layer, points)
|
||||
for location in vias:
|
||||
spot = (net_no, location)
|
||||
if spot in written or spot in self._fixed_via_points:
|
||||
continue # a via reused by another connection, or already on the board
|
||||
written.add(spot)
|
||||
result.add_via(net_no, location)
|
||||
result.connections.append((net_no, wires, vias))
|
||||
result.connection_ends.append(ends)
|
||||
@@ -1408,6 +1457,8 @@ def route_board(
|
||||
access_cost: float = 0.0,
|
||||
rollback: bool = True,
|
||||
net_widths: dict[int, int] | None = None,
|
||||
via_drill: int | None = None,
|
||||
hole_to_hole: int = 0,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -1428,5 +1479,7 @@ def route_board(
|
||||
access_cost=access_cost,
|
||||
rollback=rollback,
|
||||
net_widths=net_widths,
|
||||
via_drill=via_drill,
|
||||
hole_to_hole=hole_to_hole,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
@@ -12,6 +12,8 @@ to DSN units (dividing by the resolution) for the SES ``(wire (path ...))`` and
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import DsnBoard, parse_dsn
|
||||
from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
||||
@@ -72,6 +74,20 @@ def _mm_to_board(dsn: DsnBoard, scale: int, value_mm: float | None) -> int | Non
|
||||
return round(value_mm / unit_mm * scale)
|
||||
|
||||
|
||||
def _via_drill_board(dsn: DsnBoard, scale: int) -> int | None:
|
||||
"""The via's drill in board units, from KiCad's padstack name
|
||||
(``Via[0-1]_600:300_um`` is a 600 um pad with a 300 um drill); None when
|
||||
the name does not say, and the pad size stands in for it."""
|
||||
match = re.search(r"_([\d.]+):([\d.]+)_um", _via_padstack(dsn))
|
||||
if not match:
|
||||
return None
|
||||
return round(float(match.group(2)) * _dsn_units_per_um(dsn) * scale)
|
||||
|
||||
|
||||
def _dsn_units_per_um(dsn: DsnBoard) -> float:
|
||||
return {"um": 1.0, "mm": 0.001, "mil": 1 / 25.4, "inch": 1 / 25400}.get(dsn.unit, 1.0)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -102,6 +118,7 @@ def route_dsn_board(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
hole_to_hole_mm: float = 0.25,
|
||||
rollback: bool = True,
|
||||
) -> tuple[RouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||
@@ -128,6 +145,8 @@ def route_dsn_board(
|
||||
access_cost=access_cost,
|
||||
rollback=rollback,
|
||||
net_widths=_net_widths_board(dsn, scale),
|
||||
via_drill=_via_drill_board(dsn, scale),
|
||||
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
||||
)
|
||||
return result, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -177,6 +196,7 @@ def build_routing_result(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
hole_to_hole_mm: float = 0.25,
|
||||
rollback: bool = True,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||
@@ -185,6 +205,7 @@ def build_routing_result(
|
||||
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
hole_to_hole_mm=hole_to_hole_mm,
|
||||
rollback=rollback,
|
||||
)
|
||||
return _to_routing_result(route, dsn, scale, layer_names)
|
||||
@@ -204,6 +225,7 @@ def route_dsn_board_exact(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
hole_to_hole_mm: float = 0.25,
|
||||
rollback: bool = True,
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||
@@ -234,6 +256,8 @@ def route_dsn_board_exact(
|
||||
access_cost=access_cost,
|
||||
rollback=rollback,
|
||||
net_widths=_net_widths_board(dsn, scale),
|
||||
via_drill=_via_drill_board(dsn, scale),
|
||||
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -252,6 +276,7 @@ def build_exact_routing_result(
|
||||
seed: int | None = None,
|
||||
route_first: frozenset | None = None,
|
||||
access_cost: float = 0.0,
|
||||
hole_to_hole_mm: float = 0.25,
|
||||
rollback: bool = True,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
@@ -268,6 +293,7 @@ def build_exact_routing_result(
|
||||
seed=seed,
|
||||
route_first=route_first,
|
||||
access_cost=access_cost,
|
||||
hole_to_hole_mm=hole_to_hole_mm,
|
||||
rollback=rollback,
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Vias keep hole-to-hole spacing, even within one net."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.route.pipeline import route_dsn_board_exact
|
||||
|
||||
# four GND pads 0.5 mm apart over a GND plane: each drops a via to the plane
|
||||
_DSN = """(pcb holes (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 "Via[0-3]_500:300_um") (rule (width 150) (clearance 150)))
|
||||
(placement (component S (place G1 9000 -10000 front 0) (place G2 9500 -10000 front 0)
|
||||
(place G3 10000 -10000 front 0) (place G4 10500 -10000 front 0)))
|
||||
(library
|
||||
(image S (pin smd 1 0 0))
|
||||
(padstack smd (shape (rect F.Cu -150 -300 150 300)) (attach off))
|
||||
(padstack "Via[0-3]_500:300_um" (shape (circle F.Cu 500)) (shape (circle B.Cu 500)) (attach off)))
|
||||
(network (net GND (pins G1-1 G2-1 G3-1 G4-1))))"""
|
||||
|
||||
|
||||
def test_a_nets_own_vias_keep_hole_to_hole_spacing():
|
||||
dsn = parse_dsn(_DSN)
|
||||
exact, scale, _ = route_dsn_board_exact(dsn, resolution=2)
|
||||
assert exact.result.failed == []
|
||||
vias = exact.result.vias[1]
|
||||
assert len(set(vias)) == len(vias), "a via written twice"
|
||||
need = (300 + 250) * scale # drill + 0.25 mm, centre to centre
|
||||
for a, b in itertools.combinations(vias, 2):
|
||||
assert math.hypot(a.x - b.x, a.y - b.y) >= need, f"vias at {a} and {b} too close"
|
||||
Reference in New Issue
Block a user