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).
171 lines
6.2 KiB
Python
171 lines
6.2 KiB
Python
"""Multi-pin nets: spanning-tree pairs, and connections that end on copper
|
|
their net has already routed (a T into the tree)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from freeroute.board import build_board
|
|
from freeroute.dsn import parse_dsn
|
|
from freeroute.route import exact_router
|
|
from freeroute.route.grid_router import GridRouter, RouteResult
|
|
from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn
|
|
|
|
# P1 and P2 are nearest each other, so the tree pairs them first; P3 sits
|
|
# under the middle of that leg, 50 mm below it and 90 mm from either pin.
|
|
TEE = """(pcb "tee.dsn"
|
|
(parser (string_quote ") (space_in_quoted_tokens on) (host_cad "t") (host_version "1"))
|
|
(resolution um 10)
|
|
(unit um)
|
|
(structure
|
|
(layer F.Cu (type signal) (property (index 0)))
|
|
(boundary (path pcb 0 0 0 200000 0 200000 -100000 0 -100000 0 0))
|
|
(rule (width 2000) (clearance 2000))
|
|
)
|
|
(library
|
|
(padstack Rect_Pad (shape (rect F.Cu -1000 -1000 1000 1000)) (attach off))
|
|
(image PAD (pin Rect_Pad 1 0 0))
|
|
)
|
|
(placement
|
|
(component PAD
|
|
(place P1 60000 -20000 front 0)
|
|
(place P2 140000 -20000 front 0)
|
|
(place P3 100000 -70000 front 0)
|
|
)
|
|
)
|
|
(network
|
|
(net N (pins P1-1 P2-1 P3-1))
|
|
(class default (rule (width 2000) (clearance 2000)))
|
|
)
|
|
(wiring)
|
|
)
|
|
"""
|
|
|
|
|
|
def _router(tree_nets=True, text=TEE):
|
|
dsn = parse_dsn(text)
|
|
s = dsn.resolution.value
|
|
return GridRouter(
|
|
build_board(dsn),
|
|
trace_width=round(_rule_width_dsn(dsn) * s),
|
|
clearance=round(_rule_clearance_dsn(dsn) * s),
|
|
orthogonal=True,
|
|
tree_nets=tree_nets,
|
|
)
|
|
|
|
|
|
def test_pairs_follow_a_spanning_tree_not_the_pin_order():
|
|
router = _router()
|
|
conns = router._build_connections()
|
|
pairs = [(c.a.component_ref, c.b.component_ref) for c in conns]
|
|
assert pairs == [("P1", "P2"), ("P1", "P3")]
|
|
chain = _router(tree_nets=False)._build_connections()
|
|
assert [(c.a.component_ref, c.b.component_ref) for c in chain] == [("P1", "P2"), ("P2", "P3")]
|
|
|
|
|
|
def test_the_third_pin_tees_into_the_first_leg():
|
|
router = _router()
|
|
result = router.route()
|
|
first, second = router._conns
|
|
assert result.unrouted == 0
|
|
# the second connection starts on the first one's copper, mid-leg
|
|
start = second.path[0]
|
|
assert start in first.copper and second.a_end is None
|
|
assert second.anchors == {first.index}
|
|
assert router._dependents[first.index] == {second.index}
|
|
assert abs(router._cell_center(start[:2]).x - 1_000_000) <= router.step
|
|
# and so its copper is the 50 mm drop, not 90 mm back to a pin
|
|
assert len(second.path) < len(router._conns[0].path)
|
|
|
|
|
|
def test_without_trees_each_connection_ends_on_a_pin():
|
|
router = _router(tree_nets=False)
|
|
router.route()
|
|
assert all(c.anchors == frozenset() for c in router._conns)
|
|
assert all(c.a_end is not None and c.b_end is not None for c in router._conns)
|
|
|
|
|
|
def test_ripping_an_anchor_takes_what_hangs_on_it():
|
|
router = _router()
|
|
router.route()
|
|
first, second = router._conns
|
|
victims = router._with_dependents([first], router._conns)
|
|
assert victims == [first, second] # anchors first, as committed
|
|
for victim in victims:
|
|
router._rip(victim)
|
|
assert not first.routed and not second.routed
|
|
assert not router._dependents.get(first.index)
|
|
assert not router._cell_owner
|
|
|
|
|
|
def test_the_snapshot_lists_anchors_before_what_hangs_on_them():
|
|
router = _router()
|
|
result = router.route()
|
|
assert result.connection_anchors == [(), (0,)]
|
|
|
|
|
|
def test_the_exact_engine_drops_a_tee_whose_anchor_it_rejects(monkeypatch):
|
|
"""A connection teed onto copper the exact check refuses would dangle."""
|
|
dsn = parse_dsn(TEE)
|
|
board = build_board(dsn)
|
|
real = exact_router.route_board
|
|
|
|
def grid_then_spoil(*args, **kwargs):
|
|
result: RouteResult = real(*args, **kwargs)
|
|
assert result.connection_anchors == [(), (0,)]
|
|
# move the first leg off the board edge so the exact check rejects it
|
|
net, wires, vias = result.connections[0]
|
|
result.connections[0] = (net, [(layer, [type(p)(p.x, p.y + 2_000_000) for p in pts])
|
|
for layer, pts in wires], vias)
|
|
return result
|
|
|
|
monkeypatch.setattr(exact_router, "route_board", grid_then_spoil)
|
|
s = dsn.resolution.value
|
|
exact = exact_router.route_board_exact(
|
|
board,
|
|
trace_width=round(_rule_width_dsn(dsn) * s),
|
|
clearance=round(_rule_clearance_dsn(dsn) * s),
|
|
)
|
|
assert len(exact.result.failed) == 2 # the leg and the T hanging on it
|
|
assert not exact.result.wires
|
|
|
|
|
|
@pytest.mark.parametrize("tree_nets", [True, False])
|
|
def test_occupancy_stays_honest(tree_nets):
|
|
router = _router(tree_nets)
|
|
router.route()
|
|
conns = router._conns
|
|
assert all(conns[i].routed for i in router._cell_owner.values())
|
|
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]
|