Files
rpmroute/tests/route/test_smooth.py
T
rsp2k 34200f27ff --smooth: no 45-degree leg shorter than two track widths
A short diagonal at a wire's end is a kink beside its pad, and the pour
refilled round it left a copper sliver (KiCad copper_sliver) on the
TerraHawk lights node. Small offsets now stay square jogs; chamfers get
the same floor.
2026-10-17 11:15:47 -06:00

163 lines
6.2 KiB
Python

"""Smoothing: staircases become octilinear runs, corners are chamfered, and
nothing it keeps (ends, vias, joints, pads it crosses) moves."""
from __future__ import annotations
from rpmroute.dsn import parse_dsn
from rpmroute.route.smooth import smooth
from rpmroute.route.verify import verify_result
from rpmroute.ses.model import RoutedVia, RoutedWire, RoutingResult
VIA = "Via[0-1]_600:300_um"
def _board(extra_pads: str = "", extra_places: str = "", nets: str = "") -> str:
"""A 30 x 30 mm two-layer board, units um, 0.2 mm track and clearance.
A1 at (2, -2) mm and A2 at (22, -22) mm are net SIG."""
return f"""(pcb "smooth.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)))
(layer B.Cu (type signal) (property (index 1)))
(boundary (path pcb 0 0 0 30000 0 30000 -30000 0 -30000 0 0))
(via "{VIA}")
(rule (width 200) (clearance 200))
)
(library
(padstack P (shape (rect F.Cu -300 -300 300 300)) (attach off))
(padstack "{VIA}" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off))
(image PAD (pin P 1 0 0))
{extra_pads}
)
(placement
(component PAD (place A1 2000 -2000 front 0) (place A2 22000 -22000 front 0) {extra_places})
)
(network
(net SIG (pins A1-1 A2-1))
{nets}
(class default (rule (width 200) (clearance 200)))
)
(wiring)
)
"""
def _staircase(a, b, step):
"""Points from ``a`` to ``b`` (DSN um) in alternating x and y steps."""
(x, y), (bx, by) = a, b
pts = [(x, y)]
sx = step if bx > x else -step
sy = step if by > y else -step
while (x, y) != (bx, by):
if x != bx:
x = x + sx if abs(bx - x) > step else bx
pts.append((x, y))
if y != by:
y = y + sy if abs(by - y) > step else by
pts.append((x, y))
return pts
def _wire(pts, layer="F.Cu", fixed=False):
return RoutedWire(layer=layer, width=200, coords=[v for p in pts for v in p], fixed=fixed)
def _points(wire):
c = wire.coords
return [(round(c[i]), round(c[i + 1])) for i in range(0, len(c), 2)]
def _result(*wires, vias=(), net="SIG"):
result = RoutingResult()
for w in wires:
result.add_wire(net, w)
for x, y in vias:
result.net(net, create=True).vias.append(RoutedVia(padstack=VIA, x=x, y=y))
return result
def test_a_staircase_becomes_one_diagonal():
dsn = parse_dsn(_board())
stair = _staircase((2000, -2000), (22000, -22000), 1000)
result = _result(_wire(stair))
report = smooth(dsn, result)
pts = _points(result.nets[0].wires[0])
assert pts == [(2000, -2000), (22000, -22000)]
assert report.wires_changed == 1 and report.after.right_angles == 0
assert report.after.total_mm < report.before.total_mm
assert verify_result(dsn, result).clean
def test_a_pad_in_the_way_is_gone_round():
# another net's pad sits on the diagonal shortcut of an L-shaped route
dsn = parse_dsn(_board(extra_places="(place B1 12000 -12000 front 0) (place B2 28000 -28000 front 0)",
nets="(net OTHER (pins B1-1 B2-1))"))
result = _result(_wire([(2000, -2000), (22000, -2000), (22000, -22000)]))
report = smooth(dsn, result)
pts = _points(result.nets[0].wires[0])
assert pts[0] == (2000, -2000) and pts[-1] == (22000, -22000)
assert report.wires_changed == 1 # the corner is chamfered, short of the pad
assert verify_result(dsn, result).clean
def test_a_joint_and_a_via_stay():
dsn = parse_dsn(_board(extra_places="(place A3 22000 -2000 front 0)",
nets="").replace("(pins A1-1 A2-1)", "(pins A1-1 A2-1 A3-1)"))
stair = _staircase((2000, -2000), (22000, -22000), 1000)
joint = stair[11] # A3's leg ends here, on the staircase
leg = _wire([(22000, -2000), (joint[0], -2000), joint])
via = stair[25]
result = _result(_wire(stair), leg, vias=[via])
smooth(dsn, result)
pts = _points(result.nets[0].wires[0])
assert joint in pts and via in pts
assert pts[0] == (2000, -2000) and pts[-1] == (22000, -22000)
assert verify_result(dsn, result).clean
def test_the_board_edge_is_kept_clear_of():
# a staircase hugging the top edge: a chamfer may not push copper over it
dsn = parse_dsn(_board(extra_places="(place A3 22000 -500 front 0)").replace(
"(pins A1-1 A2-1)", "(pins A1-1 A3-1)"))
wire = _wire([(2000, -2000), (2000, -500), (22000, -500)])
result = _result(wire)
smooth(dsn, result, edge_clearance_mm=0.3)
for _x, y in _points(result.nets[0].wires[0]):
assert -y >= 300 + 100 # half width + edge clearance from y = 0
def test_fixed_wiring_is_left_alone():
dsn = parse_dsn(_board())
stair = _staircase((2000, -2000), (22000, -22000), 1000)
result = _result(_wire(stair, fixed=True))
report = smooth(dsn, result)
assert _points(result.nets[0].wires[0]) == stair
assert report.wires == 0
def test_a_detour_with_nothing_in_its_way_is_straightened():
dsn = parse_dsn(_board(extra_places="(place A3 22000 -2000 front 0)").replace(
"(pins A1-1 A2-1)", "(pins A3-1 A2-1)"))
# a U out to x = 12 mm and back, with nothing between its ends
result = _result(_wire([(22000, -2000), (12000, -2000), (12000, -22000), (22000, -22000)]))
report = smooth(dsn, result)
assert _points(result.nets[0].wires[0]) == [(22000, -2000), (22000, -22000)]
assert (report.before.right_angles, report.after.right_angles) == (2, 0)
assert verify_result(dsn, result).clean
def test_a_jog_too_small_for_a_long_diagonal_stays_square():
# a 0.1 mm jog halfway: its 45-degree leg would be shorter than two track
# widths, a kink beside the copper that leaves pour slivers
dsn = parse_dsn(_board(extra_places="(place A3 2100 -22000 front 0)").replace(
"(pins A1-1 A2-1)", "(pins A1-1 A3-1)"))
wire = [(2000, -2000), (2000, -12000), (2100, -12000), (2100, -22000)]
result = _result(_wire(wire))
smooth(dsn, result)
pts = _points(result.nets[0].wires[0])
for a, b in zip(pts, pts[1:], strict=False):
if a[0] != b[0] and a[1] != b[1]:
assert (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2 >= (2 * 200) ** 2