# -*- coding: utf-8 -*- u"""D16:拆 U13(INA226)东侧 I²C 三线束重布,连同 CH224A 的 SDA / SCL 一起接上(服务器跑)。 DCDC_ROUTE_SERVER=1 /root/pypy/bin/pypy3 .claude/r2/d16_rip.py [--extra 结果文件 ...] 为什么:CH224A 改挂主 I²C 后(用户 2026-09-12),南边唯一的 SDA 是 U13.4 —— 东边 INA_ALERT / SCL 两根线与 SDA 0.5 mm 并排走出去、西边本体底下有一段 SCL 把 2、5 脚连起来,这束一直夹着走进北边被圈住的区域,中间放不下一个孔 (d16_tap 30 个落点全不通)。用户选「拆这一束重布」,原理图不动。 做法(**只改数据、不动板**):快照里拿掉拆除区 RIP_BOX 内 RIP_NETS 的外层线 / 孔(KEEP_BOX 里 U13 本体底下那段 2—5 脚 SCL 连线留着)→ 逐网把孤立小块接到大块上:小块用自己离大块最近的焊盘,大块用它**已有铜**(线段每 0.5 mm 一点 + 孔)上离小块最近的若干点当虚拟焊盘,由近到远逐个试,接上即停;接不上的块跳过、接着试别的。 第一阶段先把 U13 的三个网接回北边主干,第二阶段再接 CH224A(U36)的两只脚。 ⚠ 三根线要挤过 U13 东北那条走廊,**先后和线宽决定挤不挤得下**(手猜两轮:SDA 先 → SCL 的 U13.2 接不上; SCL 先 → SDA 回不去)⇒ 6 种先后 × 线宽 0.2 / 0.15 共 12 组变体,每组从同一份拆完的快照起跑, 按(断的块数、孔数、线长)取最优。写 _d16_rip.out.json:{'ripped_segs', 'ripped_vias', 'segs', 'vias', 'nets', 'logs', 'variant', 'scores'};本机 d16_check 复核后 d16_push 先删后建。 """ import io import itertools import json import math import os import sys if os.environ.get('DCDC_ROUTE_SERVER') != '1': sys.exit(u'!! 布线运算只在服务器上跑(DCDC_ROUTE_SERVER=1),见 CLAUDE.md') if '--grid' not in sys.argv: sys.argv += ['--grid', '0.125'] HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) import route_nets as N # noqa: E402 R = N.R RIP_NETS = ('SDA', 'SCL', 'INA_ALERT') RIP_BOX = (15.0, 38.0, 40.0, 58.0) # U13 本体底下把 2、5 脚(都是 SCL)连起来的那段不拆:5 脚东侧空出来给 CH224A 的 SCL 落脚 KEEP_BOX = (19.5, 42.0, 22.5, 44.6) LATE = ('U36',) WIDTHS = (0.2, 0.15) TRIES = 30 # 试算开关:--tries N(每块候选落点数)/ --orders 'A,B,C;B,A'(只试这几种先后;不在里面的网拆了不接回)/ --widths 0.15,0.2 if '--tries' in sys.argv: TRIES = int(sys.argv[sys.argv.index('--tries') + 1]) ORDERS = None if '--orders' in sys.argv: ORDERS = [tuple(o.split(',')) for o in sys.argv[sys.argv.index('--orders') + 1].split(';')] # --steps 'INA_ALERT:1,SDA:1,SDA:2,SCL:1,SCL:2/...':逐步指定「网:阶段」(阶段 2 = 只接 U36 的块),几种排法用 / 隔开 # (⚠ 不用分号:remote_d16.sh 把参数不加引号地拼进远程命令,分号会把命令截断)。 # 实验 1(2026-09-12):INA→SDA→SCL 三网都接回北边,但 U13 的 SCL 新路从南边兜过 U36,把 U36.3 和 SDA 隔开 ⇒ U36.3 要排在 U13 的 SCL 前面 STEPS = None if '--steps' in sys.argv: STEPS = [[(t.split(':')[0], int(t.split(':')[1])) for t in seq.split(',')] for seq in sys.argv[sys.argv.index('--steps') + 1].split('/')] if '--widths' in sys.argv: WIDTHS = tuple(float(x) for x in sys.argv[sys.argv.index('--widths') + 1].split(',')) def inbox(x, y, b): return b[0] <= x <= b[2] and b[1] <= y <= b[3] def L(s): return math.hypot(s[3] - s[1], s[4] - s[2]) def candidates(net, segs, vias, tx, ty): c = [] for s in segs: if s[6] != net or s[0] not in (1, 2): continue n = max(1, int(L(s) / 0.5)) for k in range(n + 1): x, y = s[1] + (s[3] - s[1]) * k / n, s[2] + (s[4] - s[2]) * k / n c.append((math.hypot(x - tx, y - ty), s[0], round(x, 3), round(y, 3), max(s[5], 0.2))) for v in vias: if v[2] == net: for l in (1, 2): c.append((math.hypot(v[0] - tx, v[1] - ty), l, round(v[0], 3), round(v[1], 3), 0.6)) c.sort() out = [] for q in c: if all(math.hypot(q[2] - p[2], q[3] - p[3]) > 1.0 or q[1] != p[1] for p in out): out.append(q) if len(out) >= TRIES: break return out def run_variant(order, w, pads, base_s, base_v): segs, vias = list(base_s), list(base_v) new_s, new_v, logs, stat = [], [], [], {} def connect(net, phase): groups = N.components(net, pads, segs, vias) g0 = len(groups) failed = set() while True: groups.sort(key=len) big = groups[-1] bx = sum(q[2] for q in big) / len(big) by = sum(q[3] for q in big) / len(big) smalls = [g for g in groups[:-1] if (all(q[0] in LATE for q in g)) == (phase == 2) and frozenset((q[0], str(q[1])) for q in g) not in failed] if not smalls: break smalls.sort(key=lambda g: min(math.hypot(q[2] - bx, q[3] - by) for q in g)) sm = smalls[0] done = False for sp in sorted(sm, key=lambda q: math.hypot(q[2] - bx, q[3] - by))[:2]: for d, l, x, y, ww in candidates(net, [q for q in segs if q[6] == net], [q for q in vias if q[2] == net], sp[2], sp[3]): vp = ['#tap', '1', x, y, ww, ww, l, net, 'RECT'] N.INNER[:] = [q for q in segs if q[0] in (15, 16)] lg = [] s2, v2 = R.route_one(net, [sp, vp], w, pads + [vp], [q for q in segs if q[0] in (1, 2)], vias, [], lg) s2 = [tuple(q) for q in s2] v2 = [tuple(q) for q in v2] if len(N.components(net, pads, segs + s2, vias + v2)) < len(groups): segs.extend(s2); vias.extend(v2); new_s.extend(s2); new_v.extend(v2) logs.append(u'%s:%s.%s → L%s (%.2f,%.2f),%d 段 / %d 孔' % (net, sp[0], sp[1], l, x, y, len(s2), len(v2))) done = True break if done: break if not done: failed.add(frozenset((q[0], str(q[1])) for q in sm)) logs.append(u'%s: %s 接不上' % (net, ','.join('%s.%s' % (q[0], q[1]) for q in sm))) groups = N.components(net, pads, segs, vias) return g0, len(groups) steps = order if (order and isinstance(order[0], tuple)) else [(n, 1) for n in order] + [(n, 2) for n in ('SDA', 'SCL') if n in order] for net, phase in steps: g0, g1 = connect(net, phase) stat.setdefault(net, [g0, g1])[1] = g1 broken = sum(v[1] - 1 for v in stat.values()) score = (broken, len(new_v), round(sum(L(s) for s in new_s), 1)) return score, {'segs': [list(s) for s in new_s], 'vias': [list(v) for v in new_v], 'nets': stat, 'logs': logs} def main(): b = json.load(io.open(os.path.join(HERE, '_board_for_nets.json'), encoding='utf-8')) segs = [tuple(s[:7]) for s in b['segs']] vias = [tuple(v[:3]) for v in b['vias']] for k, a in enumerate(sys.argv): if a == '--extra': e = json.load(io.open(os.path.join(HERE, sys.argv[k + 1]), encoding='utf-8')) segs += [tuple(s[:7]) for s in e['segs']] vias += [tuple(v[:3]) for v in e['vias']] rs = [s for s in segs if s[6] in RIP_NETS and s[0] in (1, 2) and inbox(s[1], s[2], RIP_BOX) and inbox(s[3], s[4], RIP_BOX) and not (s[6] == 'SCL' and inbox(s[1], s[2], KEEP_BOX) and inbox(s[3], s[4], KEEP_BOX))] rv = [v for v in vias if v[2] in RIP_NETS and inbox(v[0], v[1], RIP_BOX)] base_s = [s for s in segs if s not in rs] base_v = [v for v in vias if v not in rv] print(u'拆除区 %s 内拆掉:%s' % (RIP_BOX, {n: (sum(1 for s in rs if s[6] == n), sum(1 for v in rv if v[2] == n)) for n in RIP_NETS})) pads = [p for p in R.load_pads() if not str(p[1]).endswith(R.TH_SUFFIX)] best, scores = None, [] for order in (STEPS or ORDERS or list(itertools.permutations(RIP_NETS))): for w in WIDTHS: sc, res = run_variant(order, w, pads, base_s, base_v) name = '→'.join(('%s:%d' % t) if isinstance(t, tuple) else t for t in order) scores.append([name, w, list(sc)]) print(u' %-44s 线宽 %.2f:断 %d 块 / %d 孔 / %.1f mm' % (name, w, sc[0], sc[1], sc[2])) if best is None or sc < best[0]: best = (sc, res, name, w) sc, res, order, w = best print(u'最优:%s 线宽 %.2f —— 断 %d 块 / %d 孔 / %.1f mm' % (order, w, sc[0], sc[1], sc[2])) for m in res['logs']: print(u' ' + m) res.update({'ripped_segs': [list(s) for s in rs], 'ripped_vias': [list(v) for v in rv], 'variant': {'order': order, 'w': w}, 'scores': scores, 'pd_refs': ['U36', 'U13']}) io.open(os.path.join(HERE, '_d16_rip.out.json'), 'w', encoding='utf-8').write(json.dumps(res, ensure_ascii=False)) if __name__ == '__main__': main()