# -*- coding: utf-8 -*- u"""下板 PCB1:**每个还要接的焊盘,能不能走得出去**。 /c/Python314/python .claude/pcb1_escape_audit.py # 全部 /c/Python314/python .claude/pcb1_escape_audit.py --net SDA # 只看这几个网络 /c/Python314/python .claude/pcb1_escape_audit.py --why U8.3 # 单个焊盘,列出挡路的铜 ## 为什么要单独有这一支 布线器报「布不通」的时候,看不出根因是**这一头出不来**还是**中间那段挤**。 两者的处理办法完全不同:前者要动扇出过孔 / 摆位(几何上就没有出路, 换多少种布线策略都一样),后者才是布线器的事。 判据:在焊盘周围开一个 8 x 8 mm 的窗,按 0.05 mm 栅格算「0.2 mm 的线能走的格」, 从出线段末端泛洪,看能不能摸到窗边。摸不到 = **这个焊盘被围死了**。 (窗开 8 mm 是因为 0.5 脚距 QFN 的死胡同都在 2~3 mm 以内; 再大就分不清「出不来」和「出得来但走不远」了。) """ import math import os import sys import numpy as np from scipy import ndimage HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import pcb1_route_sig as R # noqa: E402 E, D = R.E, R.D WIN = 4.0 # 窗口半边长(mm) G = 0.05 W = 0.20 def audit(pads, obstacles, tags, verbose_tag=None): bytag = {p['tag']: p for p in pads} stubs = getattr(audit, 'stubs', {}) alts = getattr(audit, 'alts', {}) bad = [] for tag in sorted(tags): q = bytag.get(tag) if q is None: continue # ⚠️ **每一个出线方向都要试。**只试首选方向会把 # `U13.2` 这种“南边是死胡同、北边其实通”的误报进来 —— # `route_net()` 本来就会在失败时换方向重试。 cands = list(alts.get(tag) or ([stubs[tag]] if tag in stubs else [(None, 0)])) best = None for st in cands: r = _breathe(q, st if st[0] else None, obstacles) if r is None: best = None break best = r if best is None else max(best, r) if best is None: continue bad.append((tag, q['net'], u'被围死:%d 个出线方向全试过,' u'最好的一个两层加起来只能走 %.2f mm²' % (len(cands), best))) if tag == verbose_tag: st = cands[0] sx, sy = st[0] if st[0] else (q['x'], q['y']) box = (q['x'] - WIN, q['y'] - WIN, q['x'] + WIN, q['y'] + WIN) f = E.Field(obstacles, box, skip_net=q['net'], grid=G) _why(f, q, sx, sy, obstacles, q['lay'] if q['lay'] != 12 else E.L_TOP) return bad def _breathe(q, st, obstacles): u"""从这个出线方向能不能走到窗边。能 → None;不能 → 能走的面积 mm²。否则""" if True: sx, sy = st[0] if st else (q['x'], q['y']) box = (q['x'] - WIN, q['y'] - WIN, q['x'] + WIN, q['y'] + WIN) f = E.Field(obstacles, box, skip_net=q['net'], grid=G) f.set_relax([E.sh_rect(cx, cy, hw, hh, R.pad_layers(q)) for cx, cy, hw, hh in R.pad_rects(q)] + ([E.sh_seg(q['x'], q['y'], sx, sy, st[1], q['lay'])] if st else [])) lay = q['lay'] if q['lay'] != 12 else E.L_TOP # ⚠️ **两层一起泛洪,中间按 `via_ok()` 打通。** # 只在焊盘自己那一层上算的话,「出来一步就打孔翻到底层」这种最常见的逃法 # 会被判成走不通 —— 第一版就是这样,报出 60 多个假阳性 # (`U23.9` / `U6.*` 明明单独布都能通)。 ok = {L: f.free(L, W) for L in (E.L_TOP, E.L_BOT)} vok = f.via_ok() i, j = f._idx(sx, sy) if not (0 <= i < f.nx and 0 <= j < f.ny): return None if not ok[lay][j, i]: return 0.0 seed = {L: np.zeros_like(ok[L]) for L in (E.L_TOP, E.L_BOT)} seed[lay][j, i] = True oth = {E.L_TOP: E.L_BOT, E.L_BOT: E.L_TOP} for _it in range(8): grew = False for L in (E.L_TOP, E.L_BOT): r = ndimage.binary_propagation(seed[L], mask=ok[L]) if (r != seed[L]).any(): seed[L] = r grew = True for L in (E.L_TOP, E.L_BOT): cross = seed[L] & vok & ok[oth[L]] if (cross & ~seed[oth[L]]).any(): seed[oth[L]] = seed[oth[L]] | cross grew = True if not grew: break # ⚠️ 环到哪一圈才算「出去了」:窗口外那圈墙本身是障碍, # 紧贴着它的十几格 `free()` 永远是假的(edt 不够)。 # 拿自由区最外圈当判据会把每一个焊盘都报成“出不去”。 nb = max(1, int(round(E.Field.RIM / G))) rim = nb + int(round((E.CLR + W / 2.0 + f.qerr) / G)) + 2 edge = np.zeros(ok[lay].shape, bool) edge[rim, :] = True edge[-rim - 1, :] = True edge[:, rim] = True edge[:, -rim - 1] = True if any((seed[L] & edge).any() for L in (E.L_TOP, E.L_BOT)): return None return sum(int(seed[L].sum()) for L in (E.L_TOP, E.L_BOT)) * G * G def _why(f, q, sx, sy, obstacles, lay): u"""把挡在这个焊盘 3 mm 内的异网铜按距离列出来。""" print(u' —— %s 周围 3 mm 内的异网铜(按到出线段末端的距离)——' % q['tag']) rows = [] for s, n in obstacles: if n == q['net'] or lay not in s[-1]: continue if s[0] == 'rect': d = D.seg_rect((sx, sy), (sx, sy), s[1], s[2], s[3], s[4]) what = u'焊盘/铺铜 (%.2f,%.2f) %.2fx%.2f' % (s[1], s[2], s[3] * 2, s[4] * 2) elif s[0] == 'via': d = math.hypot(sx - s[1], sy - s[2]) - s[3] what = u'过孔 (%.2f,%.2f)' % (s[1], s[2]) else: d = D.seg_pt(s[1], s[2], s[3], s[4], sx, sy) - s[5] what = u'走线 (%.2f,%.2f)-(%.2f,%.2f) w%.2f' % (s[1], s[2], s[3], s[4], s[5] * 2) if d < 3.0: rows.append((d, n, what)) for d, n, what in sorted(rows)[:14]: print(u' %6.3f %-11s %s' % (d, n or u'(空)', what)) def main(): argv = sys.argv[1:] want = None if '--net' in argv: want = set(a for a in argv[argv.index('--net') + 1:] if not a.startswith('--')) why = None if '--why' in argv: why = argv[argv.index('--why') + 1] pads, vias, lines = R.board() pours = R.pour_rects() obstacles = R.obstacles_of(pads, vias, lines, pours) js = R.jobs(pads, want, skip=() if want else R.SKIP) # ⚠️ 2026-09-06 起 `route_sig` 把出线段也下发了 —— 布不通的焊盘在板上是 # 「一个焊盘 + 一小截自己的出线段」,不再是光焊盘。只认光焊盘会报「0 个」。 # 出线段全长 ≤ 1.2 mm、没有过孔的碎片,一样当孤立焊盘审。 need = set() for net, frags in js: for fr in frags: if len(fr['pads']) != 1 or fr['vias']: continue q = fr['pads'][0] if all(max(math.hypot(l['x0'] - q['x'], l['y0'] - q['y']), math.hypot(l['x1'] - q['x'], l['y1'] - q['y'])) <= 1.2 for l in fr['lines']): need.add(q['tag']) order = [n for n, _f in js] stubs, alts = R.plan_stubs(order, pads, obstacles, need) audit.stubs = stubs audit.alts = alts if why: need = set([why]) print(u'要接的孤立焊盘 %d 个,其中留到出线段的 %d 个' % (len(need), len(stubs))) bad = audit(pads, obstacles, need, verbose_tag=why) print(u'\n**被围死的焊盘 %d 个**:' % len(bad)) for tag, net, msg in bad: print(u' [!!] %-10s %-12s %s' % (tag, net, msg)) if not bad: print(u' (没有)') return 0 if __name__ == '__main__': sys.exit(main())