A T depends on every connection between its copper and its group

Anchoring a T only on the connection whose copper it touched missed the
connections that tie that copper to the T's own group. When one of those
was ripped, the T stayed routed but cut off: on charge-sense freeroute
counted 15 unrouted while KiCad found 27 opens. Routed connections of a net
form a forest, so the path back is unique; the T now depends on all of it.
KiCad's opens and freeroute's count agree again (18 against 17, the extra
one a GND pour island left to the fill).
This commit is contained in:
2026-10-05 12:58:58 -06:00
parent b91e44e360
commit 61f51129e1
2 changed files with 52 additions and 16 deletions
+23 -16
View File
@@ -854,11 +854,14 @@ class GridRouter:
) )
return None return None
def _tree_ends(self, conn: _Connection, group) -> dict[Node, int]: def _tree_ends(self, conn: _Connection, group) -> dict[Node, frozenset[int]]:
"""Copper its net has already routed on ``group``'s side of ``conn``, """Copper its net has already routed on ``group``'s side of ``conn``: a
each node mapped to the connection that laid it: a route may start or route may start or end anywhere on it. Each node maps to what a route
end anywhere on it. Empty for plane nets, which keep their drops to ending there relies on: the connection that laid the copper and every
the plane, and when tree routing is off.""" one between it and ``group`` (routed connections of a net form a
forest, so that path is unique). Ripping any of them would cut the
route off. Empty for plane nets, which keep their drops to the plane,
and when tree routing is off."""
if not self.tree or conn.net_no in self._plane_nets: if not self.tree or conn.net_no in self._plane_nets:
return {} return {}
adjacent: dict[int, list[tuple[int, _Connection]]] = {} adjacent: dict[int, list[tuple[int, _Connection]]] = {}
@@ -868,18 +871,21 @@ class GridRouter:
a, b = id(c.a_group), id(c.b_group) a, b = id(c.a_group), id(c.b_group)
adjacent.setdefault(a, []).append((b, c)) adjacent.setdefault(a, []).append((b, c))
adjacent.setdefault(b, []).append((a, c)) adjacent.setdefault(b, []).append((a, c))
owners: dict[Node, int] = {} owners: dict[Node, frozenset[int]] = {}
seen = {id(group)} path_to: dict[int, frozenset[int]] = {id(group): frozenset()}
stack = [id(group)] stack = [id(group)]
while stack: while stack:
for other, c in adjacent.get(stack.pop(), ()): here = stack.pop()
for other, c in adjacent.get(here, ()):
if other in path_to:
continue
path = path_to[here] | {c.index}
path_to[other] = path
stack.append(other)
for node in c.copper: for node in c.copper:
# a walled cell's via moved into the pad: no copper there # a walled cell's via moved into the pad: no copper there
if node not in self._keepout and node not in self._via_only: if node not in self._keepout and node not in self._via_only:
owners.setdefault(node, c.index) owners.setdefault(node, path)
if other not in seen:
seen.add(other)
stack.append(other)
return owners return owners
def _with_dependents(self, victims: list[_Connection], conns: list[_Connection]) -> list[_Connection]: def _with_dependents(self, victims: list[_Connection], conns: list[_Connection]) -> list[_Connection]:
@@ -1943,12 +1949,13 @@ def _first(items):
def _anchors(path, starts, goals, a_tree, b_tree) -> frozenset[int]: def _anchors(path, starts, goals, a_tree, b_tree) -> frozenset[int]:
"""The connections whose copper a found path starts or ends on.""" """What a found path relies on: the connections between the copper it
found = set() starts or ends on and its own groups."""
found: set[int] = set()
if starts.get(path[0]) is None and path[0] in a_tree: if starts.get(path[0]) is None and path[0] in a_tree:
found.add(a_tree[path[0]]) found |= a_tree[path[0]]
if goals.get(path[-1]) is None and path[-1] in b_tree: if goals.get(path[-1]) is None and path[-1] in b_tree:
found.add(b_tree[path[-1]]) found |= b_tree[path[-1]]
return frozenset(found) return frozenset(found)
+29
View File
@@ -139,3 +139,32 @@ def test_occupancy_stays_honest(tree_nets):
for anchor, hanging in router._dependents.items(): for anchor, hanging in router._dependents.items():
for i in hanging: for i in hanging:
assert conns[i].routed and conns[anchor].routed assert conns[i].routed and conns[anchor].routed
# P1-P2 along the top, P3 far off to the lower right, P4 just under the
# middle of the P1-P2 leg.
CHAIN_TEE = TEE.replace(
""" (place P3 100000 -70000 front 0)
""",
""" (place P3 190000 -90000 front 0)
(place P4 100000 -40000 front 0)
""",
).replace("(pins P1-1 P2-1 P3-1)", "(pins P1-1 P2-1 P3-1 P4-1)")
def test_a_tee_depends_on_every_connection_back_to_its_group():
"""Joined as P1-P2, P2-P3, P3-P4, P4's connection tees onto P1-P2 copper,
which reaches P3's group only through P2-P3. Ripping P2-P3 must take the T
too, or it would be counted routed while cut off from P3."""
router = _router(text=CHAIN_TEE)
conns = router._conns = router._build_connections()
groups = {c.a.component_ref: c.a_group for c in conns}
groups.update({c.b.component_ref: c.b_group for c in conns})
for conn, (a, b) in zip(conns, (("P1", "P2"), ("P2", "P3"), ("P3", "P4")), strict=True):
conn.a_group, conn.b_group = groups[a], groups[b]
for conn in conns:
assert router._try_route(conn, conns)
first, second, third = conns
assert third.path[0] in first.copper # the T lands on the far leg
assert third.anchors == {first.index, second.index}
assert router._with_dependents([second], conns) == [second, third]