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