Finer routing grid: --grid-resolution K with trace halos

A cell was exactly one trace plus its clearance, so a trace owning its cell
was the whole clearance model, and the 0.4 mm grid could not reach most
pins of a 0.5 mm pitch package: between two 0.3 mm pads a trace's centre
has a legal band only 0.15 mm wide. Resolution K makes cells K times
finer and has every trace claim a halo, the cells whose centre is nearer
than trace + clearance to one of its own, released again on rip-up.

K = 1 has an empty halo and is the old grid exactly (tested). On a 0.5 mm
row, K = 4 gets every pin out where K = 1 gets two. On the rc4 boards, K = 2
cuts the sense board's opens from 66 to 41 (JAR: 32) and the power board's
to 7 (JAR: 12), still with no DRC errors from freeroute's own copper.
This commit is contained in:
2026-09-27 07:59:05 -06:00
parent 9fa5432953
commit 8929fd367e
5 changed files with 90 additions and 16 deletions
+45 -2
View File
@@ -101,6 +101,7 @@ class _Connection:
"trace_cells",
"via_cells",
"via_halo",
"halo_nodes",
)
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
@@ -115,6 +116,7 @@ class _Connection:
self.trace_cells: list[tuple[Cell, int]] = []
self.via_cells: list[Cell] = []
self.via_halo: list[Cell] = []
self.halo_nodes: list[Node] = []
class GridRouter:
@@ -135,6 +137,7 @@ class GridRouter:
orthogonal: bool = False,
via_half: int | None = None,
edge_clearance: int | None = None,
resolution: int = 1,
) -> None:
self.board = board
self.trace_width = max(trace_width, 1)
@@ -149,8 +152,21 @@ class GridRouter:
# trace copper; 8-connected also produces 45-degree segments.
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
self._orthogonal = orthogonal
# cell size must fit a trace plus its clearance to a neighbouring trace
self.step = max(self.trace_width + self.clearance, 1)
# At resolution 1 a cell is exactly one trace plus its clearance, so a
# trace owning its cell is the whole clearance model. At resolution k
# cells are k times finer, fine enough to give every pin of a 0.5 mm
# pitch row its own column, and a trace also claims a halo: the cells
# whose centre is nearer than trace + clearance to one of its own.
self.resolution = max(int(resolution), 1)
self.step = max((self.trace_width + self.clearance) // self.resolution, 1)
need = self.trace_width + self.clearance
reach = math.ceil(need / self.step)
self._halo_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
]
# A via's copper is wider than a trace. Another net's trace needs its
# centre via_half + clearance + half_width from the via's centre, so a
# via also claims every cell within via_reach of its own.
@@ -177,6 +193,8 @@ class GridRouter:
# per-connection occupancy (owner = connection index)
self._cell_owner: dict[Node, int] = {}
self._via_owner: dict[Cell, int] = {}
# trace halos: node -> indices of the connections claiming it
self._halo: dict[Node, list[int]] = {}
self._rasterize_obstacles()
# --- grid helpers -------------------------------------------------------
@@ -299,6 +317,11 @@ class GridRouter:
via_owner = self._via_owner.get(cell)
if via_owner is not None and conns[via_owner].net_no != net_no:
blocking = via_owner
if blocking is None:
for i in self._halo.get(node, ()):
if conns[i].net_no != net_no:
blocking = i
break
if blocking is None:
return 0.0
if not allow_ripup:
@@ -458,6 +481,14 @@ class GridRouter:
conn.via_cells = list(via_cells)
for cell, layer in conn.trace_cells:
self._cell_owner[(*cell, layer)] = conn.index
conn.halo_nodes = []
for (cx, cy), layer in conn.trace_cells:
for dx, dy in self._halo_offsets:
x, y = cx + dx, cy + dy
if 0 <= x < self.cols and 0 <= y < self.rows:
node = (x, y, layer)
self._halo.setdefault(node, []).append(conn.index)
conn.halo_nodes.append(node)
conn.via_halo = []
for cell in conn.via_cells:
for c in self._via_cells(cell):
@@ -492,6 +523,13 @@ class GridRouter:
if self._via_owner.get(cell) == conn.index:
del self._via_owner[cell]
conn.via_halo = []
for node in conn.halo_nodes:
claims = self._halo.get(node)
if claims is not None:
claims.remove(conn.index)
if not claims:
del self._halo[node]
conn.halo_nodes = []
conn.trace_cells = []
conn.via_cells = []
conn.wires = []
@@ -508,6 +546,9 @@ class GridRouter:
via_owner = self._via_owner.get((cx, cy))
if via_owner is not None and conns[via_owner].net_no != net_no:
owners.add(via_owner)
for i in self._halo.get((cx, cy, cl), ()):
if conns[i].net_no != net_no:
owners.add(i)
return owners
# --- passes -------------------------------------------------------------
@@ -645,6 +686,7 @@ def route_board(
orthogonal: bool = False,
via_half: int | None = None,
edge_clearance: int | None = None,
resolution: int = 1,
) -> RouteResult:
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
router = GridRouter(
@@ -658,5 +700,6 @@ def route_board(
orthogonal=orthogonal,
via_half=via_half,
edge_clearance=edge_clearance,
resolution=resolution,
)
return router.route()