--smooth: turn grid staircases into octilinear runs after routing

A last stage on the finished result, wire by wire: a run of legs between
two points becomes the octilinear shape joining them when its copper is
clear, the furthest point first; right-angle corners left are chamfered;
straight runs are merged. Ends, vias, points where another wire of the net
meets the wire, and legs across own-net pads stay. Every new leg and corner
is checked on the exact shapes against other nets, pads, keepouts and the
board edge, slanted legs both ways round as the verifier counts a pair.
A net whose pads end up joined differently is put back, and the whole
smoothing is dropped if the finished check finds more violations than
before. --smooth-report adds track length per layer.
This commit is contained in:
2026-10-17 09:35:45 -06:00
parent f3c4618fbb
commit c91ca1994c
4 changed files with 702 additions and 1 deletions
+148
View File
@@ -0,0 +1,148 @@
"""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
+1
View File
@@ -190,6 +190,7 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
"window": ["--window-reroute", "1"],
"vias": ["--reduce-vias"],
"via_cost": ["--via-cost", "40"],
"smooth": ["--smooth"],
}
for engine, options in ENGINE_OPTIONS.items():
for option in options: