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:
@@ -854,11 +854,14 @@ class GridRouter:
|
||||
)
|
||||
return None
|
||||
|
||||
def _tree_ends(self, conn: _Connection, group) -> dict[Node, int]:
|
||||
"""Copper its net has already routed on ``group``'s side of ``conn``,
|
||||
each node mapped to the connection that laid it: a route may start or
|
||||
end anywhere on it. Empty for plane nets, which keep their drops to
|
||||
the plane, and when tree routing is off."""
|
||||
def _tree_ends(self, conn: _Connection, group) -> dict[Node, frozenset[int]]:
|
||||
"""Copper its net has already routed on ``group``'s side of ``conn``: a
|
||||
route may start or end anywhere on it. Each node maps to what a route
|
||||
ending there relies on: the connection that laid the copper and every
|
||||
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:
|
||||
return {}
|
||||
adjacent: dict[int, list[tuple[int, _Connection]]] = {}
|
||||
@@ -868,18 +871,21 @@ class GridRouter:
|
||||
a, b = id(c.a_group), id(c.b_group)
|
||||
adjacent.setdefault(a, []).append((b, c))
|
||||
adjacent.setdefault(b, []).append((a, c))
|
||||
owners: dict[Node, int] = {}
|
||||
seen = {id(group)}
|
||||
owners: dict[Node, frozenset[int]] = {}
|
||||
path_to: dict[int, frozenset[int]] = {id(group): frozenset()}
|
||||
stack = [id(group)]
|
||||
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:
|
||||
# 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:
|
||||
owners.setdefault(node, c.index)
|
||||
if other not in seen:
|
||||
seen.add(other)
|
||||
stack.append(other)
|
||||
owners.setdefault(node, path)
|
||||
return owners
|
||||
|
||||
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]:
|
||||
"""The connections whose copper a found path starts or ends on."""
|
||||
found = set()
|
||||
"""What a found path relies on: the connections between the copper it
|
||||
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:
|
||||
found.add(a_tree[path[0]])
|
||||
found |= a_tree[path[0]]
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -139,3 +139,32 @@ def test_occupancy_stays_honest(tree_nets):
|
||||
for anchor, hanging in router._dependents.items():
|
||||
for i in hanging:
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user