From 21c80d30f220e2cddba20b2ef117f8611dc3a2ba Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 29 Sep 2026 09:53:05 -0600 Subject: [PATCH] Tests: fan-out stubs go outward past their pads; a plane pin gets its via --- tests/route/test_fanout.py | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/route/test_fanout.py diff --git a/tests/route/test_fanout.py b/tests/route/test_fanout.py new file mode 100644 index 0000000..f6c791e --- /dev/null +++ b/tests/route/test_fanout.py @@ -0,0 +1,78 @@ +"""--fanout: fine-pitch pins get their way out committed before routing.""" + +from __future__ import annotations + +from freeroute.board import build_board +from freeroute.board.items import Trace, Via +from freeroute.dsn import parse_dsn +from freeroute.route.fanout import FanoutRules, plan_fanout +from freeroute.route.pipeline import route_dsn_board_exact + +# a 12-pad package, 3 pads a side at 0.5 mm pitch, over a GND plane on In1; +# pad 1 (left side, top) is GND, the rest are signals to far connector pads +_SIDES = { + "L": [(-2000, y, 0) for y in (500, 0, -500)], + "R": [(2000, y, 0) for y in (500, 0, -500)], + "T": [(x, 2000, 90) for x in (-500, 0, 500)], + "B": [(x, -2000, 90) for x in (-500, 0, 500)], +} +_PADS = [p for side in "LRTB" for p in _SIDES[side]] + + +def _dsn(): + pins = "\n ".join( + f"(pin {'padv' if r else 'padh'} {i + 1} {x} {y})" for i, (x, y, r) in enumerate(_PADS) + ) + far = " ".join(f"(place C{i} {3000 + 2000 * (i % 5)} {-12000 - 2000 * (i // 5)} front 0)" for i in range(11)) + nets = "(net GND (pins U1-1))\n " + "\n ".join(f"(net S{i} (pins U1-{i + 2} C{i}-1))" for i in range(11)) + return f"""(pcb pkg (parser) (resolution um 10) (unit um) + (structure + (layer F.Cu (type signal)) (layer In1.Cu (type power)) (layer In2.Cu (type power)) + (layer B.Cu (type signal)) + (boundary (path pcb 0 -15000 15000 25000 15000 25000 -25000 -15000 -25000 -15000 15000)) + (plane GND (polygon In1.Cu 0 -14500 14500 24500 14500 24500 -24500 -14500 -24500 -14500 14500)) + (via "Via[0-3]_450:200_um") (rule (width 150) (clearance 150))) + (placement (component Q (place U1 0 0 front 0)) (component P {far})) + (library + (image Q + {pins}) + (image P (pin padc 1 0 0)) + (padstack padh (shape (rect F.Cu -400 -125 400 125)) (attach off)) + (padstack padv (shape (rect F.Cu -125 -400 125 400)) (attach off)) + (padstack padc (shape (rect F.Cu -500 -500 500 500)) (attach off)) + (padstack "Via[0-3]_450:200_um" (shape (circle F.Cu 450)) (shape (circle B.Cu 450)) (attach off))) + (network {nets}))""" + + +def _rules(dsn): + s = dsn.resolution.value + return FanoutRules(trace_width=150 * s, clearance=150 * s, via_half=225 * s, via_drill=200 * s, + hole_to_hole=250 * s, edge_clearance=0, signal_layers=[0, 3], + via_padstack="Via[0-3]_450:200_um") + + +def test_every_pin_gets_a_stub_outward_past_its_pad(): + dsn = parse_dsn(_dsn()) + board = build_board(dsn) + items = plan_fanout(board, _rules(dsn)) + stubs = [i for i in items if isinstance(i, Trace)] + assert len(stubs) == 12 + for stub in stubs: + a, b = stub.corners + pin = next(p for p in board.get_pins() if p.location == a) + pad = pin.shape.bounding_box() + # outward: the stub ends further from the package centre, outside its pad + assert abs(b.x) + abs(b.y) > abs(a.x) + abs(a.y) + assert not (pad.ll.x <= b.x <= pad.ur.x and pad.ll.y <= b.y <= pad.ur.y) + + +def test_a_plane_pin_gets_a_via_and_needs_no_route(): + dsn = parse_dsn(_dsn()) + board = build_board(dsn) + items = plan_fanout(board, _rules(dsn)) + assert len([i for i in items if isinstance(i, Via)]) == 1 + exact, _scale, _ = route_dsn_board_exact(dsn, resolution=2, fanout=True) + gnd = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "GND") + assert not any(f[0] == gnd for f in exact.result.failed) + assert exact.drc_clean + assert len(exact.result.vias.get(gnd, [])) == 1 # the fan-out's, written out