Make the grid and the exact check agree on rotated pads, shared vias and wide nets
Rotated oval pads were built from padded corners that are not in outline order, so they fell back to their bounding box; neighbouring boxes of a rotated socket overlap, and the grid's own-pad exemption opened the overlap to the pin's net, which the exact check then refused. Pads are now the convex hull of their corners, the exact tree checks a rotated pad against that outline, and the grid blocks cells against the same outline. A via reused by a second connection of its net left that connection with no claim on the via's footprint, so ripping the first owner freed cells around a via still on the board. Footprint cells now keep every claimant. A net class wider than the default was searched as a default trace: its spread was empty below half a cell, halos were circular while the exact check compares boxes, and a rip-up route only counted what its centre crossed. The spread now covers the extra width in whole cells, halos are square, and rip-up victims include what the spread crosses. bench/diagnose.py now routes with net-class widths and hole spacing, as the CLI does.
This commit is contained in:
@@ -33,7 +33,8 @@ import random
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin, Via
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
from freeroute.geometry import IntBox, IntPoint, Simplex
|
||||
from freeroute.geometry.octagon import overlaps_2d
|
||||
from freeroute.geometry.polyline import corner_box
|
||||
|
||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||
@@ -233,7 +234,7 @@ class GridRouter:
|
||||
(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
|
||||
if (dx or dy) and max(abs(dx), abs(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
|
||||
@@ -299,6 +300,9 @@ class GridRouter:
|
||||
# per-connection occupancy (owner = connection index)
|
||||
self._cell_owner: dict[Node, int] = {}
|
||||
self._via_owner: dict[Cell, int] = {}
|
||||
# every connection whose via covers a cell, in claim order: a via two
|
||||
# connections share still blocks after the first of them is ripped
|
||||
self._via_claims: dict[Cell, list[int]] = {}
|
||||
# trace halos: node -> indices of the connections claiming it
|
||||
self._halo: dict[Node, list[int]] = {}
|
||||
self._rasterize_obstacles()
|
||||
@@ -379,6 +383,19 @@ class GridRouter:
|
||||
shape.contains(IntPoint(c.x + dx, c.y + dy)) for dx in (-h, h) for dy in (-h, h)
|
||||
)
|
||||
|
||||
def _pad_cells(self, shape, box: IntBox, margin: int):
|
||||
"""Cells where a trace square ``margin`` wide around the centre overlaps
|
||||
the pad: its box when it is one, else its exact (rotated) outline,
|
||||
which is what the exact engine checks against."""
|
||||
cells = self._cells_in_box(box, margin)
|
||||
if not isinstance(shape, Simplex) or shape.is_int_box():
|
||||
yield from cells
|
||||
return
|
||||
for cell in cells:
|
||||
c = self._cell_center(cell)
|
||||
if overlaps_2d(shape, IntBox(c.x - margin, c.y - margin, c.x + margin, c.y + margin)):
|
||||
yield cell
|
||||
|
||||
def _rasterize_obstacles(self) -> None:
|
||||
margin = self.clearance + self.half_width
|
||||
outline = self.board.outline
|
||||
@@ -404,7 +421,7 @@ class GridRouter:
|
||||
continue
|
||||
net = item.net_nos[0] if item.net_nos else 0
|
||||
pin_layers = [layer for layer in item.layers if layer in self.layers]
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
for cell in self._pad_cells(item.shape, 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,
|
||||
@@ -593,18 +610,19 @@ class GridRouter:
|
||||
return self.half_width if conn.narrow else self._net_half.get(conn.net_no, self.half_width)
|
||||
|
||||
def _spread_for(self, half: int) -> list[tuple[int, int]]:
|
||||
"""Cells a trace of ``half`` also needs free: those whose centre is
|
||||
within its extra half width (plus half a cell, for the grid)."""
|
||||
"""Cells a trace of ``half`` also needs free: every cell out to its
|
||||
extra half width, rounded up to whole cells. Blocks, halos and via
|
||||
footprints are sized for a default-width trace, and copper is checked
|
||||
as boxes, so anything within that square could be too close."""
|
||||
extra = half - self.half_width
|
||||
if extra <= 0:
|
||||
return []
|
||||
reach = math.ceil(extra / self.step) + 1
|
||||
limit = extra + self.step / 2
|
||||
reach = math.ceil(extra / self.step)
|
||||
return [
|
||||
(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 <= limit
|
||||
if dx or dy
|
||||
]
|
||||
|
||||
def _halo_for(self, half: int) -> list[tuple[int, int]]:
|
||||
@@ -617,7 +635,7 @@ class GridRouter:
|
||||
(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
|
||||
if (dx or dy) and max(abs(dx), abs(dy)) * self.step < need
|
||||
]
|
||||
return offsets
|
||||
|
||||
@@ -1184,9 +1202,11 @@ class GridRouter:
|
||||
conn.via_halo = []
|
||||
for cell in conn.via_cells:
|
||||
for c in self._via_cells(cell):
|
||||
if c not in self._via_owner:
|
||||
self._via_owner[c] = conn.index
|
||||
claims = self._via_claims.setdefault(c, [])
|
||||
if conn.index not in claims:
|
||||
claims.append(conn.index)
|
||||
conn.via_halo.append(c)
|
||||
self._via_owner.setdefault(c, conn.index)
|
||||
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:
|
||||
@@ -1236,8 +1256,16 @@ class GridRouter:
|
||||
if self._cell_owner.get((*cell, layer)) == conn.index:
|
||||
del self._cell_owner[(*cell, layer)]
|
||||
for cell in conn.via_halo:
|
||||
claims = self._via_claims.get(cell)
|
||||
if claims is not None and conn.index in claims:
|
||||
claims.remove(conn.index)
|
||||
if self._via_owner.get(cell) == conn.index:
|
||||
del self._via_owner[cell]
|
||||
if claims:
|
||||
self._via_owner[cell] = claims[0]
|
||||
else:
|
||||
del self._via_owner[cell]
|
||||
if claims is not None and not claims:
|
||||
del self._via_claims[cell]
|
||||
conn.via_halo = []
|
||||
for node in conn.halo_nodes:
|
||||
claims = self._halo.get(node)
|
||||
@@ -1261,7 +1289,20 @@ class GridRouter:
|
||||
conn.rip_count += 1
|
||||
self._settle(conn, +1)
|
||||
|
||||
def _crossed_owners(self, path: list[Node], net_no: int, conns: list[_Connection]) -> set[int]:
|
||||
def _crossed_owners(
|
||||
self, path: list[Node], net_no: int, conns: list[_Connection], spread=None
|
||||
) -> set[int]:
|
||||
"""Connections whose copper or clearance ``path`` crosses; a trace
|
||||
wider than the default crosses them on its ``spread`` cells too."""
|
||||
spread = self._spread if spread is None else spread
|
||||
if spread:
|
||||
path = list(
|
||||
dict.fromkeys(
|
||||
(x + dx, y + dy, layer)
|
||||
for x, y, layer in path
|
||||
for dx, dy in ((0, 0), *spread)
|
||||
)
|
||||
)
|
||||
owners: set[int] = set()
|
||||
for cx, cy, cl in path:
|
||||
owner = self._cell_owner.get((cx, cy, cl))
|
||||
@@ -1380,7 +1421,9 @@ class GridRouter:
|
||||
path = self._search(conn, allow_ripup=True, conns=conns)
|
||||
if path is None:
|
||||
return False
|
||||
crossed = self._crossed_owners(path, conn.net_no, conns)
|
||||
crossed = self._crossed_owners(
|
||||
path, conn.net_no, conns, self._spread_for(self._conn_half(conn))
|
||||
)
|
||||
# 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):
|
||||
|
||||
Reference in New Issue
Block a user