Files
rpmroute/tests/route/test_via_spacing.py
rsp2k a6aa1991a1 Rename to rpmroute
The router began as a port of FreeRouting and has grown its own engine
(negotiated congestion, tree nets, exact checks, axis escapes), and
'freeroute' was one letter from the project it no longer follows. The
package, module and command are now rpmroute; the freeroute command stays
as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back
to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a
PCBench board routes to a byte-identical session either way.
2026-10-07 14:10:36 -06:00

137 lines
6.4 KiB
Python

"""Vias keep hole-to-hole spacing, even within one net."""
from __future__ import annotations
import itertools
import math
from rpmroute.dsn import parse_dsn
from rpmroute.route.pipeline import route_dsn_board_exact
# four GND pads 0.5 mm apart over a GND plane: each drops a via to the plane
_DSN = """(pcb holes (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 0 0 20000 0 20000 -20000 0 -20000 0 0))
(plane GND (polygon In1.Cu 0 500 -500 19500 -500 19500 -19500 500 -19500 500 -500))
(via "Via[0-3]_500:300_um") (rule (width 150) (clearance 150)))
(placement (component S (place G1 9000 -10000 front 0) (place G2 9500 -10000 front 0)
(place G3 10000 -10000 front 0) (place G4 10500 -10000 front 0)))
(library
(image S (pin smd 1 0 0))
(padstack smd (shape (rect F.Cu -150 -300 150 300)) (attach off))
(padstack "Via[0-3]_500:300_um" (shape (circle F.Cu 500)) (shape (circle B.Cu 500)) (attach off)))
(network (net GND (pins G1-1 G2-1 G3-1 G4-1))))"""
def test_a_nets_own_vias_keep_hole_to_hole_spacing():
dsn = parse_dsn(_DSN)
exact, scale, _ = route_dsn_board_exact(dsn, resolution=2)
assert exact.result.failed == []
vias = exact.result.vias[1]
assert len(set(vias)) == len(vias), "a via written twice"
need = (300 + 250) * scale # drill + 0.25 mm, centre to centre
for a, b in itertools.combinations(vias, 2):
assert math.hypot(a.x - b.x, a.y - b.y) >= need, f"vias at {a} and {b} too close"
def test_connections_sharing_a_plane_via_write_it_once():
# eight GND pads 0.4 mm apart: the hole spacing makes several of them
# end on the same plane via, which must come out once, not once each
pads = " ".join(f"(place G{i} {9000 + 400 * i} -10000 front 0)" for i in range(8))
pins = " ".join(f"G{i}-1" for i in range(8))
dsn = parse_dsn(
_DSN.replace(
"(place G1 9000 -10000 front 0) (place G2 9500 -10000 front 0)\n"
" (place G3 10000 -10000 front 0) (place G4 10500 -10000 front 0)",
pads,
)
.replace("(pins G1-1 G2-1 G3-1 G4-1)", f"(pins {pins})")
.replace("(rect F.Cu -150 -300 150 300)", "(rect F.Cu -125 -300 125 300)")
)
exact, _scale, _ = route_dsn_board_exact(dsn, resolution=2)
vias = exact.result.vias[1]
assert len(vias) < 8, "no via is shared, so this board no longer tests sharing"
assert len(set(vias)) == len(vias), "a shared via written more than once"
def _router(dsn_text, via_drill_um=300):
from rpmroute.board import build_board
from rpmroute.route.grid_router import GridRouter
from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn
dsn = parse_dsn(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), resolution=2, orthogonal=True,
via_drill=via_drill_um * s, hole_to_hole=250 * s,
), s
_THT = """(pcb tht (parser) (resolution um 10) (unit um)
(structure (layer F.Cu (type signal)) (layer B.Cu (type signal))
(boundary (path pcb 0 0 0 20000 0 20000 -20000 0 -20000 0 0))
(via "Via[0-1]_500:300_um") (rule (width 150) (clearance 150)))
(placement (component T (place J1 10000 -10000 front 0)))
(library (image T (pin tht 1 0 0))
(padstack tht (shape (circle F.Cu 1700)) (shape (circle B.Cu 1700)) (attach off))
(padstack "Via[0-1]_500:300_um" (shape (circle F.Cu 500)) (shape (circle B.Cu 500)) (attach off)))
(network (net SIG (pins J1-1))))"""
def test_no_via_goes_near_a_through_hole_pads_hole_even_of_its_own_net():
router, s = _router(_THT)
pin = router._cell_of(router.board.get_pins()[0].location)
assert not router._holes_clear(pin, 1) # own net, right on the pad
far = (pin[0] + 40, pin[1])
assert router._holes_clear(far, 1)
def test_a_via_in_a_pad_keeps_hole_spacing_from_a_fixed_via():
from rpmroute.geometry import IntPoint
text = _THT.replace("(network (net SIG (pins J1-1))))",
"(network (net SIG (pins J1-1)))\n"
" (wiring (via \"Via[0-1]_500:300_um\" 3000 -3000 (net SIG) (type route))))")
router, s = _router(text)
assert not router._hole_clear_at(IntPoint(3400 * s, -3000 * s)) # 0.4 mm < 0.55 mm
assert router._hole_clear_at(IntPoint(3600 * s, -3000 * s))
def test_hole_spacing_from_a_fixed_via_is_measured_from_where_it_really_is():
# a fixed via off the grid with its own 0.25 mm drill, and the router's
# via a smaller 0.2 mm one (the flight controller's case): a new via needs
# its centre 0.1 + 0.125 + 0.25 = 0.475 mm away, measured exactly, where
# one rule for all vias gave 0.2 + 0.25 = 0.45 from the cell centres
text = _THT.replace("(network (net SIG (pins J1-1))))",
"(network (net SIG (pins J1-1)) (net GND))\n"
" (wiring (via \"Via[0-1]_500:250_um\" 3030 -3030 (net GND) (type route))))")
text = text.replace(
'(attach off)))\n (network',
'(attach off))\n (padstack "Via[0-1]_500:250_um" (shape (circle F.Cu 500)) '
'(shape (circle B.Cu 500)) (attach off)))\n (network',
)
router, s = _router(text, via_drill_um=200)
via = next(v for v in router.board.get_items() if getattr(v, "drill", 0))
assert via.drill == 250 * s
loc = via.location
for cell in [(x, y) for x in range(router.cols) for y in range(router.rows)]:
c = router._cell_center(cell)
d = math.hypot(c.x - loc.x, c.y - loc.y)
if router.step * 1.5 < d < 470 * s:
assert not router._holes_clear(cell, 1), f"{d / s:.0f} um from the fixed via"
elif 560 * s < d < 700 * s and cell not in router._tht_block:
assert router._holes_clear(cell, 1)
def test_a_route_keeps_its_own_vias_apart():
# a route that changed layer at (10, 10) may not change layer again one
# cell away; the same cell is the same through via, and far is fine
router, _s = _router(_THT)
came_from = {(10, 10, 1): (10, 10, 0), (11, 10, 1): (10, 10, 1), (12, 10, 1): (11, 10, 1)}
assert router._near_path_via((12, 10, 1), (11, 10), came_from)
assert not router._near_path_via((12, 10, 1), (10, 10), came_from)
assert not router._near_path_via((12, 10, 1), (40, 10), came_from)