Leave fine-pitch pins straight and let rip-up reach a pin's way out
A 1.0 mm trace from a 1.27 mm pitch pin (the RN2483 row on the TTN Enschede mote) has to stay within 0.06 mm of the pin's centreline until it is past the neighbouring pads. No grid column is that close, and the router could not get there: - A stub ran as one slanted segment to its cell centre. The exact check sees that as its bounding box, which lands on the next pad. Orthogonal stubs now leave straight along the pin's axis and jog to the cell only at the end. - End nodes were chosen as in free space even in the rip-up search, so once another net crossed a pin's only exit, the pin had no end left and rip-up could never take the exit back. In rip-up mode those cells and stubs are now enterable at the rip penalty, and routes on a stub are ripped along with the ones the path crosses. - Two neighbouring pins' stubs each claim the cells a centred trace would crowd, so their claims overlapped well before their copper did. Stub against stub is now checked on the copper. - Escapes also look one column either side, so two pins leaving the same way can jog apart. TTN Enschede mote, exact engine with --shove: 12 unrouted before, 0 after; KiCad DRC on the result has no clearance violations and no unconnected items. The 0.5 mm pitch row test at resolution 1 now routes all 5 pins (it pinned 2).
This commit is contained in:
@@ -27,6 +27,7 @@ DRC-clean geometry, and blind/buried via spans.
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
import math
|
||||
import random
|
||||
|
||||
@@ -123,6 +124,7 @@ class _Connection:
|
||||
"path",
|
||||
"narrow",
|
||||
"half",
|
||||
"stubs",
|
||||
)
|
||||
|
||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
||||
@@ -138,6 +140,8 @@ class _Connection:
|
||||
self.via_cells: list[Cell] = []
|
||||
self.via_halo: list[Cell] = []
|
||||
self.halo_nodes: list[Node] = []
|
||||
# each end's stub: its layer, its copper tiles, and the nodes it claims
|
||||
self.stubs: list[tuple[int, list[IntBox], set[Node]]] = []
|
||||
# the pins and zones each end may use, and which pin the route ended
|
||||
# on (None when it ended on a zone)
|
||||
self.a_group: tuple[list[Pin], list] = ([a], [])
|
||||
@@ -634,8 +638,8 @@ class GridRouter:
|
||||
if not conn.b_group[0] and any(z.layer in self._power_layers for z in conn.b_group[1]):
|
||||
# the plane is everywhere: aim at where the route starts instead
|
||||
goal_cell = self._group_target(conn.a_group)
|
||||
starts = self._group_ends(conn.a_group, conn.net_no, conns)
|
||||
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
||||
starts = self._group_ends(conn.a_group, conn.net_no, conns, allow_ripup)
|
||||
goals = self._group_ends(conn.b_group, conn.net_no, conns, allow_ripup)
|
||||
if not starts or not goals:
|
||||
return None
|
||||
# A start that is already an end joins at once, except that ending on
|
||||
@@ -656,10 +660,19 @@ class GridRouter:
|
||||
came_from: dict[Node, Node] = {}
|
||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
||||
# a pin end costs its stub; a zone or plane end is entered normally
|
||||
goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if isinstance(p, Pin)}
|
||||
# (and in rip-up mode the rip penalty of whatever lies on it)
|
||||
goal_stub = {
|
||||
n: self._stub_cost(p, n) + self._end_penalty(p, n, net_no, allow_ripup, conns)
|
||||
for n, p in goals.items()
|
||||
if isinstance(p, Pin)
|
||||
}
|
||||
for s in sorted(starts):
|
||||
pin = starts[s]
|
||||
g_score[s] = self._stub_cost(pin, s) if isinstance(pin, Pin) else 0.0
|
||||
g_score[s] = (
|
||||
self._stub_cost(pin, s) + self._end_penalty(pin, s, net_no, allow_ripup, conns)
|
||||
if isinstance(pin, Pin)
|
||||
else 0.0
|
||||
)
|
||||
heapq.heappush(open_heap, (g_score[s] + self._h(s, goal_cell), s))
|
||||
|
||||
# Via footprints are checked lazily: a layer change goes on the heap as
|
||||
@@ -790,24 +803,52 @@ class GridRouter:
|
||||
math.hypot(loc.x - point.x, loc.y - point.y) < reach for loc, reach in self._tht_holes
|
||||
)
|
||||
|
||||
def _stub_points(self, pin_location: IntPoint, cell: Cell) -> list[IntPoint]:
|
||||
"""The stub's corners, from the pin to its cell centre.
|
||||
|
||||
The direct segment is fine for 45-degree routing. Orthogonal routing
|
||||
leaves the pin straight along the axis it is further off the cell on
|
||||
and jogs across to the centre at the end: an L of two flush boxes,
|
||||
which is what the exact check sees. A single slanted segment is
|
||||
checked as its bounding box there, and a 1 mm trace to a column
|
||||
0.1 mm off a 1.27 mm pitch pin's centreline then lands inside the
|
||||
clearance of the pad next door, though the straight exit clears it.
|
||||
"""
|
||||
end = self._cell_center(cell)
|
||||
dx, dy = end.x - pin_location.x, end.y - pin_location.y
|
||||
if not self._orthogonal or dx == 0 or dy == 0:
|
||||
return [pin_location, end]
|
||||
if abs(dx) >= abs(dy):
|
||||
return [pin_location, IntPoint(end.x, pin_location.y), end]
|
||||
return [pin_location, IntPoint(pin_location.x, end.y), end]
|
||||
|
||||
def _stub_cells(self, pin_location: IntPoint, cell: Cell, half: int | None = None) -> list[Cell]:
|
||||
"""Cells where another net's trace would come too close to the stub."""
|
||||
from freeroute.board.outline import _point_segment
|
||||
|
||||
end = self._cell_center(cell)
|
||||
points = self._stub_points(pin_location, cell)
|
||||
end = points[-1]
|
||||
reach = (half or self._cur_half) + self.half_width + self.clearance
|
||||
box = IntBox(
|
||||
min(pin_location.x, end.x), min(pin_location.y, end.y),
|
||||
max(pin_location.x, end.x), max(pin_location.y, end.y),
|
||||
)
|
||||
legs = list(zip(points, points[1:], strict=False))
|
||||
return [
|
||||
c
|
||||
for c in self._cells_in_box(box, reach)
|
||||
if _point_segment(self._cell_center(c), pin_location, end) < reach
|
||||
if min(_point_segment(self._cell_center(c), a, b) for a, b in legs) < reach
|
||||
]
|
||||
|
||||
def _stub_clear(
|
||||
self, pin: Pin, cell: Cell, layer: int, net_no: int, conns, keepouts_only: bool = False
|
||||
self,
|
||||
pin: Pin,
|
||||
cell: Cell,
|
||||
layer: int,
|
||||
net_no: int,
|
||||
conns,
|
||||
keepouts_only: bool = False,
|
||||
allow_ripup: bool = False,
|
||||
) -> bool:
|
||||
keepouts = self._keepout_boxes.get(layer)
|
||||
if keepouts:
|
||||
@@ -815,9 +856,9 @@ class GridRouter:
|
||||
# engine will: its copper grown by clearance must miss every keepout
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
end = self._cell_center(cell)
|
||||
if end != pin.location:
|
||||
for tile in PolylineShape(Polyline([pin.location, end]), self._cur_half).tiles():
|
||||
points = self._stub_points(pin.location, cell)
|
||||
if points[-1] != pin.location:
|
||||
for tile in PolylineShape(Polyline(points), self._cur_half).tiles():
|
||||
grown = tile.offset(self.clearance)
|
||||
if any(k.overlaps(grown if needs else tile) for k, needs in keepouts):
|
||||
return False
|
||||
@@ -827,14 +868,60 @@ class GridRouter:
|
||||
return False
|
||||
if keepouts_only:
|
||||
return True
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
owner = self._cell_owner.get((*c, layer))
|
||||
if owner is not None and conns[owner].net_no != net_no:
|
||||
return False
|
||||
owners = self._stub_owners(pin, cell, layer, net_no, conns)
|
||||
# in rip-up mode a route on the stub is one more to rip, unless it may not be
|
||||
return not owners or (
|
||||
allow_ripup and all(conns[i].rip_count < self.rip_cap for i in owners)
|
||||
)
|
||||
|
||||
def _stub_tiles(self, pin_location: IntPoint, cell: Cell, half: int) -> list[IntBox]:
|
||||
"""The stub's copper: its legs, and a square at each corner and at the
|
||||
cell end, where the route carries on."""
|
||||
points = self._stub_points(pin_location, cell)
|
||||
tiles = [corner_box(point, half) for point in points[1:]]
|
||||
if len(points) > 1 and points[-1] != pin_location:
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
tiles += PolylineShape(Polyline(points), half).tiles()
|
||||
return tiles
|
||||
|
||||
def _stub_owners(
|
||||
self, pin: Pin, cell: Cell, layer: int, net_no: int, conns, half: int | None = None
|
||||
) -> set[int]:
|
||||
"""Other nets' routes whose copper the stub from ``pin`` to ``cell`` would touch."""
|
||||
owners: set[int] = set()
|
||||
grown = None
|
||||
for c in self._stub_cells(pin.location, cell, half):
|
||||
node = (*c, layer)
|
||||
owner = self._cell_owner.get(node)
|
||||
if owner is not None and conns[owner].net_no != net_no and owner not in owners:
|
||||
# Both stubs are off the grid, and each claims the cells a
|
||||
# trace centred there would crowd, so two stubs side by side
|
||||
# (neighbouring pins leaving straight) overlap in claims well
|
||||
# before their copper comes near. Check those exactly.
|
||||
other = [tiles for lay, tiles, nodes in conns[owner].stubs if node in nodes]
|
||||
if other:
|
||||
if grown is None:
|
||||
grown = [
|
||||
t.offset(self.clearance)
|
||||
for t in self._stub_tiles(pin.location, cell, half or self._cur_half)
|
||||
]
|
||||
if not any(g.overlaps(t) for tiles in other for t in tiles for g in grown):
|
||||
continue
|
||||
owners.add(owner)
|
||||
via_owner = self._via_owner.get(c)
|
||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||
return False
|
||||
return True
|
||||
owners.add(via_owner)
|
||||
return owners
|
||||
|
||||
def _end_penalty(self, pin: Pin, node: Node, net_no: int, allow_ripup: bool, conns) -> float:
|
||||
"""Rip-up cost of starting or ending on ``node``: the routes on its
|
||||
cell and on the stub to it, each at the penalty entering it would cost."""
|
||||
if not allow_ripup:
|
||||
return 0.0
|
||||
owners = self._stub_owners(pin, node[:2], node[2], net_no, conns)
|
||||
owners |= self._crossed_owners([node], net_no, conns)
|
||||
return sum(self.rip_penalty * (1 + conns[i].rip_count) for i in owners)
|
||||
|
||||
def _zone_nodes(self, zone) -> list[Node]:
|
||||
"""Cells of a zone where a via of this net fits inside its copper; for a
|
||||
@@ -882,11 +969,13 @@ class GridRouter:
|
||||
return zone.name or "zone"
|
||||
return ""
|
||||
|
||||
def _group_ends(self, group, net_no: int, conns) -> dict[Node, Pin | None]:
|
||||
def _group_ends(
|
||||
self, group, net_no: int, conns, allow_ripup: bool = False
|
||||
) -> dict[Node, Pin | None]:
|
||||
"""End nodes of a group, each mapped to its pin (None for a zone cell)."""
|
||||
ends: dict[Node, Pin | None] = {}
|
||||
for pin in group[0]:
|
||||
for node in sorted(self._end_nodes(pin, net_no, conns)):
|
||||
for node in sorted(self._end_nodes(pin, net_no, conns, allow_ripup)):
|
||||
ends.setdefault(node, pin)
|
||||
for zone in group[1]:
|
||||
plane = zone.layer in self._power_layers
|
||||
@@ -908,10 +997,13 @@ class GridRouter:
|
||||
return (dx + dy) / self.step
|
||||
return math.hypot(dx, dy) / self.step
|
||||
|
||||
def _end_nodes(self, pin: Pin, net_no: int, conns) -> set[Node]:
|
||||
def _end_nodes(self, pin: Pin, net_no: int, conns, allow_ripup: bool = False) -> set[Node]:
|
||||
"""Nodes a route may start or end on for ``pin``: the nearest cell and
|
||||
its neighbours, where the cell is enterable and the stub to it is clear.
|
||||
A crowded fine-pitch pin gets more than one way out."""
|
||||
A crowded fine-pitch pin gets more than one way out. In rip-up mode a
|
||||
cell or stub another route is on counts as enterable, as it does
|
||||
everywhere else, or a pin whose only way out another net has taken
|
||||
could never have it back."""
|
||||
layers = [layer for layer in pin.layers if layer in self.layers]
|
||||
cx, cy = self._cell_of(pin.location)
|
||||
nodes: set[Node] = set()
|
||||
@@ -920,16 +1012,16 @@ class GridRouter:
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
continue
|
||||
for layer in layers:
|
||||
if (dx or dy) and self._enter_cost(cell, layer, net_no, False, conns) is None:
|
||||
if (dx or dy) and self._enter_cost(cell, layer, net_no, allow_ripup, conns) is None:
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns, allow_ripup=allow_ripup):
|
||||
nodes.add((*cell, layer))
|
||||
# Also offer exact escape stubs: a fine-pitch pin's nearby cells can be
|
||||
# a pocket inside its own pad that leads nowhere (the grid columns miss
|
||||
# the narrow legal band past the pad tip). The stub's length is its
|
||||
# cost, so it is only taken where a short step would not do.
|
||||
if self._orthogonal and self._crowded(pin, net_no) and self._trapped(pin, net_no, nodes):
|
||||
nodes |= self._escape_nodes(pin, net_no, conns, layers, (cx, cy))
|
||||
nodes |= self._escape_nodes(pin, net_no, conns, layers, (cx, cy), allow_ripup)
|
||||
if not nodes: # nothing clear: fall back to the nearest cell, as before
|
||||
nodes = {(cx, cy, layer) for layer in layers}
|
||||
if self._keepout_boxes and not any(
|
||||
@@ -995,14 +1087,16 @@ class GridRouter:
|
||||
self._trapped_cache[key] = trapped
|
||||
return trapped
|
||||
|
||||
def _escape_nodes(self, pin: Pin, net_no: int, conns, layers, origin: Cell) -> set[Node]:
|
||||
def _escape_nodes(
|
||||
self, pin: Pin, net_no: int, conns, layers, origin: Cell, allow_ripup: bool = False
|
||||
) -> set[Node]:
|
||||
"""For a pin walled in on the grid (a fine-pitch pin whose legal band
|
||||
the grid columns miss): look along each axis for the first open cell
|
||||
whose straight stub from the pin clears every other net's pad by the
|
||||
full margin, measured exactly rather than on the grid."""
|
||||
from freeroute.board.outline import _segment_box
|
||||
whose stub from the pin clears every other net's pad by the full
|
||||
clearance, checked on the stub's copper tiles as the exact engine
|
||||
will rather than on the grid."""
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
margin = self.half_width + self.clearance
|
||||
reach = self._escape_cells
|
||||
near = [
|
||||
(p.shape.bounding_box(), set(p.layers))
|
||||
@@ -1014,24 +1108,33 @@ class GridRouter:
|
||||
]
|
||||
cx, cy = origin
|
||||
nodes: set[Node] = set()
|
||||
for dx, dy in _NEIGHBOURS[:4]:
|
||||
# the pin's own column or row and the one either side: two pins side
|
||||
# by side can then jog apart at the end of their straight exits
|
||||
for (dx, dy), side in itertools.product(_NEIGHBOURS[:4], (0, -1, 1)):
|
||||
sx, sy = (side, 0) if dx == 0 else (0, side)
|
||||
nodes_before = len(nodes)
|
||||
for k in range(2, reach + 1):
|
||||
cell = (cx + dx * k, cy + dy * k)
|
||||
cell = (cx + dx * k + sx, cy + dy * k + sy)
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
break
|
||||
end = self._cell_center(cell)
|
||||
grown = [
|
||||
tile.offset(self.clearance)
|
||||
for tile in PolylineShape(
|
||||
Polyline(self._stub_points(pin.location, cell)), self._cur_half
|
||||
).tiles()
|
||||
]
|
||||
for layer in layers:
|
||||
if self._enter_cost(cell, layer, net_no, False, conns) is None:
|
||||
if self._enter_cost(cell, layer, net_no, allow_ripup, conns) is None:
|
||||
continue
|
||||
if any(
|
||||
layer in box_layers and _segment_box(pin.location, end, box) < margin
|
||||
layer in box_layers and any(g.overlaps(box) for g in grown)
|
||||
for box, box_layers in near
|
||||
):
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns, allow_ripup=allow_ripup):
|
||||
nodes.add((*cell, layer))
|
||||
if any(n[:2] == cell for n in nodes):
|
||||
break # the first open cell along this axis is enough
|
||||
if len(nodes) > nodes_before:
|
||||
break # the first open cell along this line is enough
|
||||
return nodes
|
||||
|
||||
def _relax(self, current, nxt, tentative, goal_cell, g_score, came_from, open_heap):
|
||||
@@ -1085,15 +1188,19 @@ class GridRouter:
|
||||
self._via_owner[c] = conn.index
|
||||
conn.via_halo.append(c)
|
||||
ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else ()
|
||||
conn.stubs = []
|
||||
for pin, (layer, cells), at in ends:
|
||||
if not isinstance(pin, Pin):
|
||||
continue # ended on a zone or a plane: no stub
|
||||
cell = cells[at]
|
||||
claimed: set[Node] = set()
|
||||
for c in self._stub_cells(pin.location, cell, half):
|
||||
node = (*c, layer)
|
||||
if node not in self._cell_owner:
|
||||
self._cell_owner[node] = conn.index
|
||||
conn.trace_cells.append((c, layer))
|
||||
claimed.add(node)
|
||||
conn.stubs.append((layer, self._stub_tiles(pin.location, cell, half), claimed))
|
||||
# A pin inside a wire keepout can only leave by a via. Its stub to the
|
||||
# cell centre would be track inside the keepout, so the via goes on
|
||||
# the pin itself (a via in its own pad) and the stub is left out.
|
||||
@@ -1111,9 +1218,9 @@ class GridRouter:
|
||||
continue
|
||||
points = [self._cell_center(c) for c in cells]
|
||||
if idx == 0 and isinstance(conn.a_end, Pin):
|
||||
points.insert(0, conn.a_end.location)
|
||||
points[0:0] = self._stub_points(conn.a_end.location, cells[0])[:-1]
|
||||
if idx == len(wire_segs) - 1 and isinstance(conn.b_end, Pin):
|
||||
points.append(conn.b_end.location)
|
||||
points.extend(self._stub_points(conn.b_end.location, cells[-1])[-2::-1])
|
||||
points = _simplify(points)
|
||||
if len(points) >= 2:
|
||||
conn.wires.append((layer, points))
|
||||
@@ -1140,6 +1247,7 @@ class GridRouter:
|
||||
del self._halo[node]
|
||||
conn.halo_nodes = []
|
||||
conn.trace_cells = []
|
||||
conn.stubs = []
|
||||
for cell in conn.via_cells:
|
||||
count = self._via_nets.get(cell)
|
||||
if count and count.get(conn.net_no):
|
||||
@@ -1272,7 +1380,14 @@ class GridRouter:
|
||||
path = self._search(conn, allow_ripup=True, conns=conns)
|
||||
if path is None:
|
||||
return False
|
||||
victims = [conns[i] for i in sorted(self._crossed_owners(path, conn.net_no, conns))]
|
||||
crossed = self._crossed_owners(path, conn.net_no, conns)
|
||||
# and the routes on the stubs from the pins to the path's ends
|
||||
for end, node in ((conn.a_end, path[0]), (conn.b_end, path[-1])):
|
||||
if isinstance(end, Pin):
|
||||
crossed |= self._stub_owners(
|
||||
end, node[:2], node[2], conn.net_no, conns, self._conn_half(conn)
|
||||
)
|
||||
victims = [conns[i] for i in sorted(crossed)]
|
||||
if self.rollback:
|
||||
return self._ripup_transaction(conn, path, victims, conns)
|
||||
for victim in victims:
|
||||
|
||||
Reference in New Issue
Block a user