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:
+10
-1
@@ -110,6 +110,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
help="disable rip-up-and-retry (grid and exact engines)",
|
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("--layers", help="comma-separated signal layer indices to route on")
|
||||||
|
parser.add_argument(
|
||||||
|
"--grid-resolution",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
metavar="K",
|
||||||
|
help="grid and exact engines: cells K times finer than trace + clearance "
|
||||||
|
"(default 1); 2 or more reaches fine-pitch pins, at some cost in time",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--edge-clearance",
|
"--edge-clearance",
|
||||||
type=float,
|
type=float,
|
||||||
@@ -166,6 +174,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
rip_up=rip_up,
|
rip_up=rip_up,
|
||||||
max_passes=max_passes,
|
max_passes=max_passes,
|
||||||
edge_clearance_mm=args.edge_clearance,
|
edge_clearance_mm=args.edge_clearance,
|
||||||
|
resolution=args.grid_resolution,
|
||||||
)
|
)
|
||||||
elif args.engine == "room":
|
elif args.engine == "room":
|
||||||
result = build_rooms_routing_result(
|
result = build_rooms_routing_result(
|
||||||
@@ -175,7 +184,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
else:
|
else:
|
||||||
result = build_routing_result(
|
result = build_routing_result(
|
||||||
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=args.edge_clearance,
|
edge_clearance_mm=args.edge_clearance, resolution=args.grid_resolution,
|
||||||
)
|
)
|
||||||
|
|
||||||
ses_text = write_ses(dsn, result)
|
ses_text = write_ses(dsn, result)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def route_board_exact(
|
|||||||
shove_cap: int = 3,
|
shove_cap: int = 3,
|
||||||
via_half: int | None = None,
|
via_half: int | None = None,
|
||||||
edge_clearance: int | None = None,
|
edge_clearance: int | None = None,
|
||||||
|
resolution: int = 1,
|
||||||
) -> ExactRouteResult:
|
) -> ExactRouteResult:
|
||||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||||
|
|
||||||
@@ -118,6 +119,7 @@ def route_board_exact(
|
|||||||
orthogonal=True,
|
orthogonal=True,
|
||||||
via_half=via_half,
|
via_half=via_half,
|
||||||
edge_clearance=edge_clearance,
|
edge_clearance=edge_clearance,
|
||||||
|
resolution=resolution,
|
||||||
)
|
)
|
||||||
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
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class _Connection:
|
|||||||
"trace_cells",
|
"trace_cells",
|
||||||
"via_cells",
|
"via_cells",
|
||||||
"via_halo",
|
"via_halo",
|
||||||
|
"halo_nodes",
|
||||||
)
|
)
|
||||||
|
|
||||||
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:
|
||||||
@@ -115,6 +116,7 @@ class _Connection:
|
|||||||
self.trace_cells: list[tuple[Cell, int]] = []
|
self.trace_cells: list[tuple[Cell, int]] = []
|
||||||
self.via_cells: list[Cell] = []
|
self.via_cells: list[Cell] = []
|
||||||
self.via_halo: list[Cell] = []
|
self.via_halo: list[Cell] = []
|
||||||
|
self.halo_nodes: list[Node] = []
|
||||||
|
|
||||||
|
|
||||||
class GridRouter:
|
class GridRouter:
|
||||||
@@ -135,6 +137,7 @@ class GridRouter:
|
|||||||
orthogonal: bool = False,
|
orthogonal: bool = False,
|
||||||
via_half: int | None = None,
|
via_half: int | None = None,
|
||||||
edge_clearance: int | None = None,
|
edge_clearance: int | None = None,
|
||||||
|
resolution: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.board = board
|
self.board = board
|
||||||
self.trace_width = max(trace_width, 1)
|
self.trace_width = max(trace_width, 1)
|
||||||
@@ -149,8 +152,21 @@ class GridRouter:
|
|||||||
# trace copper; 8-connected also produces 45-degree segments.
|
# trace copper; 8-connected also produces 45-degree segments.
|
||||||
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
||||||
self._orthogonal = orthogonal
|
self._orthogonal = orthogonal
|
||||||
# cell size must fit a trace plus its clearance to a neighbouring trace
|
# At resolution 1 a cell is exactly one trace plus its clearance, so a
|
||||||
self.step = max(self.trace_width + self.clearance, 1)
|
# 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
|
# 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
|
# centre via_half + clearance + half_width from the via's centre, so a
|
||||||
# via also claims every cell within via_reach of its own.
|
# via also claims every cell within via_reach of its own.
|
||||||
@@ -177,6 +193,8 @@ class GridRouter:
|
|||||||
# per-connection occupancy (owner = connection index)
|
# per-connection occupancy (owner = connection index)
|
||||||
self._cell_owner: dict[Node, int] = {}
|
self._cell_owner: dict[Node, int] = {}
|
||||||
self._via_owner: dict[Cell, 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()
|
self._rasterize_obstacles()
|
||||||
|
|
||||||
# --- grid helpers -------------------------------------------------------
|
# --- grid helpers -------------------------------------------------------
|
||||||
@@ -299,6 +317,11 @@ class GridRouter:
|
|||||||
via_owner = self._via_owner.get(cell)
|
via_owner = self._via_owner.get(cell)
|
||||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||||
blocking = via_owner
|
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:
|
if blocking is None:
|
||||||
return 0.0
|
return 0.0
|
||||||
if not allow_ripup:
|
if not allow_ripup:
|
||||||
@@ -458,6 +481,14 @@ class GridRouter:
|
|||||||
conn.via_cells = list(via_cells)
|
conn.via_cells = list(via_cells)
|
||||||
for cell, layer in conn.trace_cells:
|
for cell, layer in conn.trace_cells:
|
||||||
self._cell_owner[(*cell, layer)] = conn.index
|
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 = []
|
conn.via_halo = []
|
||||||
for cell in conn.via_cells:
|
for cell in conn.via_cells:
|
||||||
for c in self._via_cells(cell):
|
for c in self._via_cells(cell):
|
||||||
@@ -492,6 +523,13 @@ class GridRouter:
|
|||||||
if self._via_owner.get(cell) == conn.index:
|
if self._via_owner.get(cell) == conn.index:
|
||||||
del self._via_owner[cell]
|
del self._via_owner[cell]
|
||||||
conn.via_halo = []
|
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.trace_cells = []
|
||||||
conn.via_cells = []
|
conn.via_cells = []
|
||||||
conn.wires = []
|
conn.wires = []
|
||||||
@@ -508,6 +546,9 @@ class GridRouter:
|
|||||||
via_owner = self._via_owner.get((cx, cy))
|
via_owner = self._via_owner.get((cx, cy))
|
||||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||||
owners.add(via_owner)
|
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
|
return owners
|
||||||
|
|
||||||
# --- passes -------------------------------------------------------------
|
# --- passes -------------------------------------------------------------
|
||||||
@@ -645,6 +686,7 @@ def route_board(
|
|||||||
orthogonal: bool = False,
|
orthogonal: bool = False,
|
||||||
via_half: int | None = None,
|
via_half: int | None = None,
|
||||||
edge_clearance: int | None = None,
|
edge_clearance: int | None = None,
|
||||||
|
resolution: int = 1,
|
||||||
) -> 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(
|
||||||
@@ -658,5 +700,6 @@ def route_board(
|
|||||||
orthogonal=orthogonal,
|
orthogonal=orthogonal,
|
||||||
via_half=via_half,
|
via_half=via_half,
|
||||||
edge_clearance=edge_clearance,
|
edge_clearance=edge_clearance,
|
||||||
|
resolution=resolution,
|
||||||
)
|
)
|
||||||
return router.route()
|
return router.route()
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ def _via_half_board(dsn: DsnBoard, scale: int) -> int | None:
|
|||||||
def route_dsn_board(
|
def route_dsn_board(
|
||||||
dsn: DsnBoard,
|
dsn: DsnBoard,
|
||||||
*,
|
*,
|
||||||
|
resolution: int = 1,
|
||||||
edge_clearance_mm: float | None = None,
|
edge_clearance_mm: float | None = None,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
@@ -91,6 +92,7 @@ def route_dsn_board(
|
|||||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
result = route_board(
|
result = route_board(
|
||||||
board,
|
board,
|
||||||
|
resolution=resolution,
|
||||||
via_half=_via_half_board(dsn, scale),
|
via_half=_via_half_board(dsn, scale),
|
||||||
edge_clearance=edge_clearance_board,
|
edge_clearance=edge_clearance_board,
|
||||||
trace_width=width_board,
|
trace_width=width_board,
|
||||||
@@ -130,6 +132,7 @@ def _to_routing_result(route, dsn: DsnBoard, scale: int, layer_names: list[str])
|
|||||||
def build_routing_result(
|
def build_routing_result(
|
||||||
dsn: DsnBoard,
|
dsn: DsnBoard,
|
||||||
*,
|
*,
|
||||||
|
resolution: int = 1,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
@@ -138,7 +141,7 @@ def build_routing_result(
|
|||||||
"""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(
|
||||||
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,
|
edge_clearance_mm=edge_clearance_mm, resolution=resolution,
|
||||||
)
|
)
|
||||||
return _to_routing_result(route, dsn, scale, layer_names)
|
return _to_routing_result(route, dsn, scale, layer_names)
|
||||||
|
|
||||||
@@ -146,6 +149,7 @@ def build_routing_result(
|
|||||||
def route_dsn_board_exact(
|
def route_dsn_board_exact(
|
||||||
dsn: DsnBoard,
|
dsn: DsnBoard,
|
||||||
*,
|
*,
|
||||||
|
resolution: int = 1,
|
||||||
edge_clearance_mm: float | None = None,
|
edge_clearance_mm: float | None = None,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
@@ -165,6 +169,7 @@ def route_dsn_board_exact(
|
|||||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
exact = route_board_exact(
|
exact = route_board_exact(
|
||||||
board,
|
board,
|
||||||
|
resolution=resolution,
|
||||||
via_half=_via_half_board(dsn, scale),
|
via_half=_via_half_board(dsn, scale),
|
||||||
edge_clearance=edge_clearance_board,
|
edge_clearance=edge_clearance_board,
|
||||||
trace_width=width_board,
|
trace_width=width_board,
|
||||||
@@ -181,6 +186,7 @@ def route_dsn_board_exact(
|
|||||||
def build_exact_routing_result(
|
def build_exact_routing_result(
|
||||||
dsn: DsnBoard,
|
dsn: DsnBoard,
|
||||||
*,
|
*,
|
||||||
|
resolution: int = 1,
|
||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
shove: bool = False,
|
shove: bool = False,
|
||||||
diagonal: bool = False,
|
diagonal: bool = False,
|
||||||
@@ -197,6 +203,7 @@ def build_exact_routing_result(
|
|||||||
rip_up=rip_up,
|
rip_up=rip_up,
|
||||||
max_passes=max_passes,
|
max_passes=max_passes,
|
||||||
edge_clearance_mm=edge_clearance_mm,
|
edge_clearance_mm=edge_clearance_mm,
|
||||||
|
resolution=resolution,
|
||||||
)
|
)
|
||||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||||
|
|
||||||
@@ -255,6 +262,7 @@ def route(
|
|||||||
pack: bool = False,
|
pack: bool = False,
|
||||||
diagonal: bool = False,
|
diagonal: bool = False,
|
||||||
edge_clearance_mm: float | None = None,
|
edge_clearance_mm: float | None = None,
|
||||||
|
resolution: int = 1,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Route a Specctra DSN string and return the routed SES string.
|
"""Route a Specctra DSN string and return the routed SES string.
|
||||||
|
|
||||||
@@ -269,12 +277,15 @@ def route(
|
|||||||
dsn = parse_dsn(dsn_text)
|
dsn = parse_dsn(dsn_text)
|
||||||
if engine == "exact":
|
if engine == "exact":
|
||||||
result = build_exact_routing_result(
|
result = build_exact_routing_result(
|
||||||
dsn, layers=layers, shove=shove, diagonal=diagonal, edge_clearance_mm=edge_clearance_mm
|
dsn, layers=layers, shove=shove, diagonal=diagonal,
|
||||||
|
edge_clearance_mm=edge_clearance_mm, resolution=resolution,
|
||||||
)
|
)
|
||||||
elif engine == "room":
|
elif engine == "room":
|
||||||
result = build_rooms_routing_result(
|
result = build_rooms_routing_result(
|
||||||
dsn, layers=layers, shove=shove, pack=pack, edge_clearance_mm=edge_clearance_mm
|
dsn, layers=layers, shove=shove, pack=pack, edge_clearance_mm=edge_clearance_mm
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = build_routing_result(dsn, layers=layers, edge_clearance_mm=edge_clearance_mm)
|
result = build_routing_result(
|
||||||
|
dsn, layers=layers, edge_clearance_mm=edge_clearance_mm, resolution=resolution
|
||||||
|
)
|
||||||
return write_ses(dsn, result)
|
return write_ses(dsn, result)
|
||||||
|
|||||||
@@ -51,16 +51,25 @@ def test_routes_keep_edge_clearance_at_a_rounded_corner():
|
|||||||
assert outline.edge_distance(p) >= half + 500 * scale - 1
|
assert outline.edge_distance(p) >= half + 500 * scale - 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
@pytest.mark.parametrize(("resolution", "escape"), [(1, 2), (4, 5)])
|
||||||
strict=True,
|
def test_every_pin_of_a_half_millimetre_pitch_row_escapes(resolution, escape):
|
||||||
reason="known gap: the grid step (trace + clearance, 0.4 mm here) does not line up "
|
# Five 0.3 mm pads at 0.5 mm pitch, each wired to its own pad 8 mm away.
|
||||||
"with a 0.5 mm pin row, so only the outer pins get a column; the FreeRouting JAR, "
|
# Between two neighbours a trace's centre has a legal band only 0.15 mm
|
||||||
"being gridless, routes all five. This is most of the gap on an LQFP.",
|
# wide. At resolution 1 the 0.4 mm grid misses most bands; at 4 its
|
||||||
)
|
# 0.1 mm columns cannot, and every pin gets out, as the JAR manages.
|
||||||
def test_every_pin_of_a_half_millimetre_pitch_row_escapes():
|
|
||||||
# five pads at 0.5 mm pitch, each wired to its own pad 8 mm away
|
|
||||||
places = " ".join(f"(place R{i} {6000 + 500 * i} -6000 front 0)" for i in range(5))
|
places = " ".join(f"(place R{i} {6000 + 500 * i} -6000 front 0)" for i in range(5))
|
||||||
places += " " + " ".join(f"(place F{i} {6000 + 500 * i} -14000 front 0)" for i in range(5))
|
places += " " + " ".join(f"(place F{i} {6000 + 500 * i} -14000 front 0)" for i in range(5))
|
||||||
nets = " ".join(f"(net N{i} (pins R{i}-1 F{i}-1))" for i in range(5))
|
nets = " ".join(f"(net N{i} (pins R{i}-1 F{i}-1))" for i in range(5))
|
||||||
result, _, _ = route_dsn_board_exact(parse_dsn(_dsn(f"(component P {places})", nets)))
|
result, _, _ = route_dsn_board_exact(
|
||||||
assert len(result.routed_net_numbers) == 5
|
parse_dsn(_dsn(f"(component P {places})", nets)), resolution=resolution
|
||||||
|
)
|
||||||
|
assert len(result.routed_net_numbers) == escape
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolution_one_has_no_halo_so_the_grid_is_unchanged():
|
||||||
|
from freeroute.route.grid_router import GridRouter
|
||||||
|
|
||||||
|
board = build_board(parse_dsn(_dsn("", "")))
|
||||||
|
assert GridRouter(board, trace_width=2500, clearance=1500)._halo_offsets == []
|
||||||
|
fine = GridRouter(board, trace_width=2500, clearance=1500, resolution=2)
|
||||||
|
assert fine.step == 2000 and (1, 0) in fine._halo_offsets and (2, 0) not in fine._halo_offsets
|
||||||
|
|||||||
Reference in New Issue
Block a user