Rip every user of a shared via, and judge vias round at the outline
Two connections of a net can share a via. The grid kept one owner per via cell next to the claim list, and the search and rip-up read only that owner: a route of another net through the via ripped the first user and left the second's via under the new trace. The exact check then threw one of them out. The search and rip-up now read the claim list, and the single owner is gone. The exact check also tested a via against the outline as its square box, whose corner reaches past a 45-degree chamfer where the round via does not. The grid keeps the via's centre its radius plus the edge clearance from the outline, as KiCad measures it; the exact check now does the same. kitspace_minisumo_v3 seed 4: 4 rejected connections to 0, 11 failed to 7.
This commit is contained in:
@@ -207,7 +207,9 @@ def route_board_exact(
|
|||||||
for i, (box, box_layers) in enumerate(boxes)
|
for i, (box, box_layers) in enumerate(boxes)
|
||||||
for layer in box_layers
|
for layer in box_layers
|
||||||
) or _end_in_rule_area(tree, wires, wire_half) or (
|
) or _end_in_rule_area(tree, wires, wire_half) or (
|
||||||
outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)
|
outline is not None and not _inside_outline(
|
||||||
|
outline, boxes[:wire_count], vias, (via_half or wire_half) + edge, edge
|
||||||
|
)
|
||||||
):
|
):
|
||||||
rejected.append((net_no, *ends))
|
rejected.append((net_no, *ends))
|
||||||
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||||
@@ -670,6 +672,15 @@ def _copper_boxes(wires, vias, half_width, via_layers, via_half=None):
|
|||||||
return boxes
|
return boxes
|
||||||
|
|
||||||
|
|
||||||
|
def _inside_outline(outline, wire_boxes, vias, via_reach, edge) -> bool:
|
||||||
|
"""Copper inside the board, ``edge`` from its outline. A via is round, as
|
||||||
|
the grid and KiCad take it: its square box would reach past a chamfered
|
||||||
|
corner where the via itself does not."""
|
||||||
|
return all(outline.box_clear(b, edge) for b, _ in wire_boxes) and all(
|
||||||
|
outline.point_clear(v, via_reach) for v in vias
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_rotated(shape) -> bool:
|
def _is_rotated(shape) -> bool:
|
||||||
return isinstance(shape, Simplex) and not shape.is_int_box()
|
return isinstance(shape, Simplex) and not shape.is_int_box()
|
||||||
|
|
||||||
|
|||||||
@@ -299,9 +299,9 @@ class GridRouter:
|
|||||||
self._zone_cells: dict[int, list[Node]] = {}
|
self._zone_cells: dict[int, list[Node]] = {}
|
||||||
# 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] = {}
|
|
||||||
# every connection whose via covers a cell, in claim order: a via two
|
# every connection whose via covers a cell, in claim order: a via two
|
||||||
# connections share still blocks after the first of them is ripped
|
# connections share still blocks after the first of them is ripped,
|
||||||
|
# and a route through it must rip all of them
|
||||||
self._via_claims: dict[Cell, list[int]] = {}
|
self._via_claims: dict[Cell, list[int]] = {}
|
||||||
# trace halos: node -> indices of the connections claiming it
|
# trace halos: node -> indices of the connections claiming it
|
||||||
self._halo: dict[Node, list[int]] = {}
|
self._halo: dict[Node, list[int]] = {}
|
||||||
@@ -520,9 +520,7 @@ class GridRouter:
|
|||||||
if owner is not None and conns[owner].net_no != net_no:
|
if owner is not None and conns[owner].net_no != net_no:
|
||||||
blocking = owner
|
blocking = owner
|
||||||
else:
|
else:
|
||||||
via_owner = self._via_owner.get(cell)
|
blocking = self._via_blocker(cell, net_no, conns)
|
||||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
|
||||||
blocking = via_owner
|
|
||||||
if blocking is None:
|
if blocking is None:
|
||||||
for i in self._halo.get(node, ()):
|
for i in self._halo.get(node, ()):
|
||||||
if conns[i].net_no != net_no:
|
if conns[i].net_no != net_no:
|
||||||
@@ -537,6 +535,17 @@ class GridRouter:
|
|||||||
return None
|
return None
|
||||||
return self.rip_penalty * (1 + rip_count)
|
return self.rip_penalty * (1 + rip_count)
|
||||||
|
|
||||||
|
def _via_others(self, cell: Cell, net_no: int, conns: list[_Connection]) -> list[int]:
|
||||||
|
"""Connections of other nets whose via covers ``cell``: all of them,
|
||||||
|
since two connections of a net can share one via."""
|
||||||
|
return [i for i in self._via_claims.get(cell, ()) if conns[i].net_no != net_no]
|
||||||
|
|
||||||
|
def _via_blocker(self, cell: Cell, net_no: int, conns: list[_Connection]) -> int | None:
|
||||||
|
"""The other net's via connection on ``cell`` hardest to rip (the
|
||||||
|
penalty and the rip cap go by it), or None."""
|
||||||
|
others = self._via_others(cell, net_no, conns)
|
||||||
|
return max(others, key=lambda i: conns[i].rip_count) if others else None
|
||||||
|
|
||||||
def _via_cells(self, cell: Cell) -> list[Cell]:
|
def _via_cells(self, cell: Cell) -> list[Cell]:
|
||||||
"""The cell a via sits in plus the neighbours its copper reaches."""
|
"""The cell a via sits in plus the neighbours its copper reaches."""
|
||||||
r = self.via_reach
|
r = self.via_reach
|
||||||
@@ -927,9 +936,7 @@ class GridRouter:
|
|||||||
if not any(g.overlaps(t) for tiles in other for t in tiles for g in grown):
|
if not any(g.overlaps(t) for tiles in other for t in tiles for g in grown):
|
||||||
continue
|
continue
|
||||||
owners.add(owner)
|
owners.add(owner)
|
||||||
via_owner = self._via_owner.get(c)
|
owners.update(self._via_others(c, net_no, conns))
|
||||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
|
||||||
owners.add(via_owner)
|
|
||||||
return owners
|
return owners
|
||||||
|
|
||||||
def _end_penalty(self, pin: Pin, node: Node, net_no: int, allow_ripup: bool, conns) -> float:
|
def _end_penalty(self, pin: Pin, node: Node, net_no: int, allow_ripup: bool, conns) -> float:
|
||||||
@@ -1206,7 +1213,6 @@ class GridRouter:
|
|||||||
if conn.index not in claims:
|
if conn.index not in claims:
|
||||||
claims.append(conn.index)
|
claims.append(conn.index)
|
||||||
conn.via_halo.append(c)
|
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 ()
|
ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else ()
|
||||||
conn.stubs = []
|
conn.stubs = []
|
||||||
for pin, (layer, cells), at in ends:
|
for pin, (layer, cells), at in ends:
|
||||||
@@ -1259,11 +1265,6 @@ class GridRouter:
|
|||||||
claims = self._via_claims.get(cell)
|
claims = self._via_claims.get(cell)
|
||||||
if claims is not None and conn.index in claims:
|
if claims is not None and conn.index in claims:
|
||||||
claims.remove(conn.index)
|
claims.remove(conn.index)
|
||||||
if self._via_owner.get(cell) == conn.index:
|
|
||||||
if claims:
|
|
||||||
self._via_owner[cell] = claims[0]
|
|
||||||
else:
|
|
||||||
del self._via_owner[cell]
|
|
||||||
if claims is not None and not claims:
|
if claims is not None and not claims:
|
||||||
del self._via_claims[cell]
|
del self._via_claims[cell]
|
||||||
conn.via_halo = []
|
conn.via_halo = []
|
||||||
@@ -1308,9 +1309,7 @@ class GridRouter:
|
|||||||
owner = self._cell_owner.get((cx, cy, cl))
|
owner = self._cell_owner.get((cx, cy, cl))
|
||||||
if owner is not None and conns[owner].net_no != net_no:
|
if owner is not None and conns[owner].net_no != net_no:
|
||||||
owners.add(owner)
|
owners.add(owner)
|
||||||
via_owner = self._via_owner.get((cx, cy))
|
owners.update(self._via_others((cx, cy), net_no, conns))
|
||||||
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), ()):
|
for i in self._halo.get((cx, cy, cl), ()):
|
||||||
if conns[i].net_no != net_no:
|
if conns[i].net_no != net_no:
|
||||||
owners.add(i)
|
owners.add(i)
|
||||||
|
|||||||
@@ -143,11 +143,48 @@ def test_a_shared_via_keeps_its_footprint_when_one_user_is_ripped():
|
|||||||
router._rip(first)
|
router._rip(first)
|
||||||
|
|
||||||
assert second.routed and via in second.via_cells
|
assert second.routed and via in second.via_cells
|
||||||
assert all(router._via_owner.get(c) == second.index for c in footprint)
|
assert all(router._via_claims.get(c) == [second.index] for c in footprint)
|
||||||
# another net still keeps its distance from the via that is left
|
# another net still keeps its distance from the via that is left
|
||||||
assert all(router._enter_cost_base(c, 0, sig.net_no, False, conns) is None for c in footprint)
|
assert all(router._enter_cost_base(c, 0, sig.net_no, False, conns) is None for c in footprint)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_route_through_a_shared_via_rips_every_user():
|
||||||
|
"""Two connections of a net share a via. Another net routed through it
|
||||||
|
in rip-up mode has to rip both, or the one left keeps its via under the
|
||||||
|
new trace and the exact check throws one of them out."""
|
||||||
|
dsn = parse_dsn(_SHARED_VIA)
|
||||||
|
s = dsn.resolution.value
|
||||||
|
router = GridRouter(
|
||||||
|
build_board(dsn),
|
||||||
|
trace_width=round(_rule_width_dsn(dsn) * s),
|
||||||
|
clearance=round(_rule_clearance_dsn(dsn) * s),
|
||||||
|
via_half=_via_half_board(dsn, s),
|
||||||
|
)
|
||||||
|
conns = router._conns = router._build_connections()
|
||||||
|
gnd = [c for c in conns if len({c.a.component_ref, c.b.component_ref} & {"G1", "G2", "G3"})]
|
||||||
|
sig = next(c for c in conns if c not in gnd)
|
||||||
|
first, second = gnd
|
||||||
|
g3 = router._cell_of(next(p for p in router.board.get_pins() if p.component_ref == "G3").location)
|
||||||
|
via = (g3[0] - 10, g3[1])
|
||||||
|
|
||||||
|
def through_via(conn):
|
||||||
|
a, b = router._cell_of(conn.a.location), router._cell_of(conn.b.location)
|
||||||
|
return _line(a, via, 0) + _line(via, b, 1)
|
||||||
|
|
||||||
|
router._commit(first, through_via(first))
|
||||||
|
router._commit(second, through_via(second))
|
||||||
|
# the via's copper alone: footprint cells no trace or trace halo covers
|
||||||
|
crossing = [
|
||||||
|
node
|
||||||
|
for c in router._via_cells(via)
|
||||||
|
for node in ((*c, 0), (*c, 1))
|
||||||
|
if node not in router._cell_owner and node not in router._halo
|
||||||
|
]
|
||||||
|
assert crossing
|
||||||
|
|
||||||
|
assert router._crossed_owners(crossing, sig.net_no, conns) == {first.index, second.index}
|
||||||
|
|
||||||
|
|
||||||
# --- a wider net class --------------------------------------------------------
|
# --- a wider net class --------------------------------------------------------
|
||||||
|
|
||||||
_WIDE = """(pcb wide (parser) (resolution um 10) (unit um)
|
_WIDE = """(pcb wide (parser) (resolution um 10) (unit um)
|
||||||
@@ -225,3 +262,23 @@ def test_a_pad_cell_check_uses_the_rotated_outline():
|
|||||||
c = router._cell_center(cell)
|
c = router._cell_center(cell)
|
||||||
square = IntBox(c.x - margin, c.y - margin, c.x + margin, c.y + margin)
|
square = IntBox(c.x - margin, c.y - margin, c.x + margin, c.y + margin)
|
||||||
assert oval.shape.intersection(square).dimension() != 2
|
assert oval.shape.intersection(square).dimension() != 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_via_by_a_chamfered_corner_is_judged_round():
|
||||||
|
"""The grid keeps a via's centre its radius plus the edge clearance from
|
||||||
|
the outline. The exact check used the via's square, whose corner reaches
|
||||||
|
past a 45-degree chamfer where the round via does not."""
|
||||||
|
from freeroute.board.outline import BoardOutline
|
||||||
|
from freeroute.geometry import IntBox, IntPoint
|
||||||
|
from freeroute.route.exact_router import _inside_outline
|
||||||
|
|
||||||
|
# a 10 mm square with its lower-left corner cut at 45 degrees
|
||||||
|
outline = BoardOutline([IntPoint(*p) for p in
|
||||||
|
[(30, 0), (100, 0), (100, 100), (0, 100), (0, 30)]])
|
||||||
|
half, edge = 4, 1
|
||||||
|
via = IntPoint(19, 20) # its centre 6.4 from the chamfer x + y = 30
|
||||||
|
square = IntBox(via.x - half, via.y - half, via.x + half, via.y + half)
|
||||||
|
assert not outline.box_clear(square, edge) # the square's corner: 0.7 away
|
||||||
|
assert _inside_outline(outline, [], [via], half + edge, edge)
|
||||||
|
# 4.2 from the chamfer: the round via itself is too close
|
||||||
|
assert not _inside_outline(outline, [], [IntPoint(18, 18)], half + edge, edge)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def test_occupancy_only_holds_routed_connections(name):
|
|||||||
router, _ = _routed_router(name)
|
router, _ = _routed_router(name)
|
||||||
conns = router._conns
|
conns = router._conns
|
||||||
assert all(conns[i].routed for i in router._cell_owner.values())
|
assert all(conns[i].routed for i in router._cell_owner.values())
|
||||||
assert all(conns[i].routed for i in router._via_owner.values())
|
assert all(conns[i].routed for claims in router._via_claims.values() for i in claims)
|
||||||
assert all(conns[i].routed for claims in router._halo.values() for i in claims)
|
assert all(conns[i].routed for claims in router._halo.values() for i in claims)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user