"""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]