Merge fix/minisumo: shared-via owners for search and rip-up, round vias against the outline

This commit is contained in:
2026-10-03 11:31:01 -06:00
4 changed files with 87 additions and 20 deletions
+12 -1
View File
@@ -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()
+16 -17
View File
@@ -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)
+58 -1
View File
@@ -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)
+1 -1
View File
@@ -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)