# -*- coding: utf-8 -*- u"""没布通的网:可达区域审计(**只在服务器跑** —— 占用图上泛洪,属布线运算)。 DCDC_ROUTE_SERVER=1 /root/pypy/bin/pypy3 .claude/r2/reach_audit.py [--nets A,B,...] [--cache 文件] 对每个「没布通 / 出不来」的网,用 A* 同一张占用图(`route_base.grid_now`:焊盘 / 已布铜 / 12V 大铜 / 天线净空 / NPTH / 板边,按本网线宽膨胀),从本网已有的铜和焊盘出发两层泛洪(8 邻接,换层要 free_via), 报:分成几个连通域、每个域里有哪些脚、域的大小与外框、**把它围住的是哪些网**(边界格点按归属号计数)。 ⚠ 用的是**布完之后**的铜:围住它的墙可能是在它之后才布的。每个挡路网后面标了它在布线顺序里的位置 (缓存 segs 里第一段的下标 / 总段数,越小越早 —— 构造铜排在最前)。 2026-09-11 起用:第 26 轮切线量化表明 U20 一带失败的 13 个网全是端点被围,不是通道容量不够。 """ import base64 import collections import io import json import os import sys if os.environ.get('DCDC_ROUTE_SERVER') != '1': sys.exit(u'!! 可达审计是占用图上的布线运算,只在服务器 10.5.5.13 跑(见 CLAUDE.md 布线计算硬规则)') for a, v in (('--offline', None), ('--grid', '0.125')): if a not in sys.argv: sys.argv += [a] + ([v] if v else []) sys.stdout.reconfigure(encoding='utf-8') HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(HERE)) sys.path.insert(0, HERE) import route_base as R # noqa: E402 RT = R.RT def arg(name, default=None): return sys.argv[sys.argv.index(name) + 1] if name in sys.argv else default def flood(g, net, starts): u"""从 starts [(l,i,j)] 两层泛洪(和 A* 同判据:g.free / 换层 g.free_via)。返回 (seen, 格点数)。""" NX, NY = RT.NX, RT.NY seen = {l: bytearray(NX * NY) for l in RT.LAY} dq = collections.deque() for (l, i, j) in starts: o = j * NX + i if not seen[l][o] and g.free(l, i, j, net): seen[l][o] = 1 dq.append((l, i, j)) n = 0 while dq: l, i, j = dq.popleft() n += 1 for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)): ii, jj = i + di, j + dj if 0 <= ii < NX and 0 <= jj < NY and not seen[l][jj * NX + ii] and g.free(l, ii, jj, net): seen[l][jj * NX + ii] = 1 dq.append((l, ii, jj)) ol = 2 if l == 1 else 1 if not seen[ol][j * NX + i] and g.free(ol, i, j, net) and g.free_via(i, j, net): seen[ol][j * NX + i] = 1 dq.append((ol, i, j)) return seen, n def starts_mode(d, pads, bv, segs, vias): u"""`--starts`:照 route_net 的起步方式复核每条「没布通」—— 失败的那只脚(扇出落点只在 L1)泛洪, 看碰不碰得到本网离它 3 mm 以外的铜;再对照「落点那颗孔在 L4 一侧也许起步」。""" import re NX, G = RT.NX, RT.G pm = {(q[0], str(q[1])): q for q in pads} pat = re.compile(u'^(\\S+): (\\S+?)\\.(\\S+) (没布通|出不来)$') print(u'\n=== --starts:按 route_net 的起步方式复核(最终占用图;「够得着」= 碰到本网 3 mm 外的铜)===') for m in d['logs']: mm = pat.match(m) if not mm: continue net, ref, pin, why = mm.groups() if net in R.PLANE_NETS or net in R.DP_NETS: continue w = RT.WIDTH.get(net, RT.W_DEFAULT) halfw = w / 2.0 + RT.CLR g, gt, others = R.grid_now(pads, halfw, segs, vias, bv) k = g.nid(net) if pin.startswith('fan:'): rp = pm.get((ref, pin[4:])) if rp is None: print(u' %-12s %s.%s:找不到原焊盘' % (net, ref, pin)) continue cand = sorted((abs(v[0] - rp[2]) + abs(v[1] - rp[3]), v) for v in vias if v[2] == net) if not cand or cand[0][0] > 4.0: print(u' %-12s %s.%s:找不到扇出孔' % (net, ref, pin)) continue v = cand[0][1] p = [ref, pin, v[0], v[1], 0.3, 0.3, 1, net, 'RECT'] oth = [] else: p = pm.get((ref, pin)) if p is None: print(u' %-12s %s.%s:找不到焊盘' % (net, ref, pin)) continue oth = others.get(p[6], []) sh = RT.stub_w(p, w) / 2.0 st = RT.pad_nodes(g, p, halfw, oth, sh, gt) def reach(seen): for l in RT.LAY: a, s = g.own[l], seen[l] for o in range(len(a)): if s[o] and a[o] == k: j, i = divmod(o, NX) if abs(i * G - p[2]) + abs(j * G - p[3]) > 3.0: return True return False seen, n1 = flood(g, net, st) r1 = reach(seen) line = u' %-12s %-12s %s:出口 %2d 个;原样起步 可达 %7.1f mm² 够得着=%s' % ( net, '%s.%s' % (ref, pin), why, len(st), n1 * G * G / 2.0, u'是' if r1 else u'否') if pin.startswith('fan:'): i0, j0 = RT.gx(p[2]), RT.gy(p[3]) st2 = st + [(2, i, j) for (_l, i, j) in st] + [(2, i0, j0)] seen2, n2 = flood(g, net, st2) line += u' | 孔的 L4 侧也许起步 可达 %7.1f mm² 够得着=%s' % (n2 * G * G / 2.0, u'是' if reach(seen2) else u'否') print(line) sys.stdout.flush() def replay_mode(d, pads, bv, segs, vias, nets): u"""`--replay`:在最终占用图上**原样调 RT.route_net(mutate=False)**重放这些网 —— 扇出过的脚(FANOUT_REFS 且旁边 4 mm 内有本网的孔)换成 L1 虚拟落点,和 plan_all 里一样。 最终图的障碍只比布线当时多:这里布得通 ⇒ 当时图里多了最终图没有的东西;这里也不通 ⇒ 原因在 route_net 里。""" print(u'\n=== --replay:最终占用图上重放 route_net ===') for net in nets: w = RT.WIDTH.get(net, RT.W_DEFAULT) halfw = w / 2.0 + RT.CLR g, gt, others = R.grid_now(pads, halfw, segs, vias, bv) ps = [] for q in pads: if q[7] != net or q[6] not in RT.LAY or str(q[1]).endswith(R.TH_SUFFIX): continue if q[0] in R.FANOUT_REFS: cand = sorted((abs(v[0] - q[2]) + abs(v[1] - q[3]), v) for v in vias if v[2] == net) if cand and cand[0][0] < 4.0: v = cand[0][1] ps.append([q[0], 'fan:%s' % q[1], v[0], v[1], 0.3, 0.3, 1, net, 'RECT']) continue ps.append(list(q)) logs = [] s, v, _st = RT.route_net(g, net, ps, halfw, w, others, logs, gt, mutate=False) L = sum(((a[3] - a[1]) ** 2 + (a[4] - a[2]) ** 2) ** 0.5 for a in s) print(u' %-12s 脚 %s → %s(%.1f mm / %d 孔)' % ( net, ' '.join('%s.%s' % (q[0], q[1]) for q in ps), u'布得通' if not logs else u';'.join(logs), L, len(v))) sys.stdout.flush() def dbg_cmp(nets, pads, bv, segs, vias): u"""`--dbg-cmp A,B`:route_base --dbg-nets 存下的「失败那一刻」占用图 vs 同一次运行的最终占用图,逐格比。 报「当时对本网封着、最终图对本网是空的」格点按归属网分组的数目与外框 —— 最终图的铜只比当时多,这类格点本不该有。""" print(u'\n=== --dbg-cmp:失败那一刻 vs 最终占用图 ===') for net in nets: f = os.path.join(HERE, '_dbg_%s.json' % net.replace('#', '_')) if not os.path.exists(f): print(u' %s:没有 %s(这次没失败?)' % (net, os.path.basename(f))) continue d = json.load(io.open(f, encoding='utf-8')) halfw = d['halfw'] g2, _gt2, _o = R.grid_now(pads, halfw, segs, vias, bv) k1, k2 = d['ids'].get(net), g2.nid(net) inv1 = {v: n for n, v in d['ids'].items()} print(u'\n■ %s(halfw %.3f)失败日志:%s' % (net, halfw, u';'.join(d['logs']))) print(u' 脚:%s' % ' '.join('%s.%s@L%d(%.2f,%.2f)' % (p[0], p[1], p[6], p[2], p[3]) for p in d['pads'])) for key, arr2 in (('own1', g2.own[1]), ('own2', g2.own[2]), ('ownv1', g2.ownv[1]), ('ownv2', g2.ownv[2])): a1 = base64.b64decode(d[key]) cnt = collections.Counter() box = {} NX, G = d['NX'], d['G'] for o in range(len(a1)): v1, v2 = a1[o], arr2[o] if v1 != 0 and v1 != k1 and (v2 == 0 or v2 == k2): nm = inv1.get(v1, u'多网' if v1 == 255 else '?') cnt[nm] += 1 j, i = divmod(o, NX) b = box.get(nm) box[nm] = (min(b[0], i * G), min(b[1], j * G), max(b[2], i * G), max(b[3], j * G)) if b else (i * G, j * G, i * G, j * G) tot = sum(cnt.values()) print(u' %-5s 当时封、最终空:%d 格%s' % (key, tot, '' if not tot else u' —— ' + ' '.join( u'%s×%d[x %.1f~%.1f y %.1f~%.1f]' % (n, c, box[n][0], box[n][2], box[n][1], box[n][3]) for n, c in cnt.most_common(8)))) if key == 'own1' and tot: # 抽样:差异格点离「该网焊盘」和「该网最终走线」各多远 —— 判断是引出脚、干线还是别的 NXd = d['NX'] smp = collections.defaultdict(list) for o in range(len(a1)): v1, v2 = a1[o], arr2[o] if v1 != 0 and v1 != k1 and (v2 == 0 or v2 == k2): nm = inv1.get(v1, '?') if len(smp[nm]) < 400: j, i = divmod(o, NXd) smp[nm].append((i * G, j * G)) for nm, c in cnt.most_common(4): pp = [q for q in pads if q[7] == nm] ss = [s for s in segs if s[6] == nm] dp = [min([abs(x - q[2]) - q[4] / 2 if abs(x - q[2]) > q[4] / 2 else 0 for q in pp] or [99]) for (x, y) in smp[nm][:1]] dpad = sorted(min([max(abs(x - q[2]) - q[4] / 2, abs(y - q[3]) - q[5] / 2, 0) for q in pp] or [99]) for (x, y) in smp[nm]) dseg = sorted(min([R._seg_seg(s, (0, x, y, x, y)) - s[5] / 2 for s in ss] or [99]) for (x, y) in smp[nm]) md = lambda a: a[len(a) // 2] if a else 99 # noqa: E731 print(u' %-10s 样本 %d 格:离本网焊盘 中位 %.2f / 最大 %.2f mm;离本网最终走线 中位 %.2f / 最小 %.2f mm;例 %s' % ( nm, len(smp[nm]), md(dpad), dpad[-1], md(dseg), dseg[0], ' '.join('(%.2f,%.2f)' % t for t in smp[nm][:4]))) _ = dp # 失败那一刻的图上直接泛洪:从本网一只脚附近的铜出发,够不够得着另一只脚附近的铜 g1 = RT.Grid() g1._id = dict(d['ids']) for l, key in ((1, 'own1'), (2, 'own2')): g1.own[l] = bytearray(base64.b64decode(d[key])) for l, key in ((1, 'ownv1'), (2, 'ownv2')): g1.ownv[l] = bytearray(base64.b64decode(d[key])) NXd, Gd = d['NX'], d['G'] own_k = [(l, o) for l in RT.LAY for o in range(len(g1.own[l])) if g1.own[l][o] == k1] def near(p, r=1.5): out = [] for (l, o) in own_k: j, i = divmod(o, NXd) if abs(i * Gd - p[2]) <= r + p[4] / 2 and abs(j * Gd - p[3]) <= r + p[5] / 2: out.append((l, i, j)) return out pl = d['pads'] for a in range(len(pl)): for b in range(a + 1, len(pl)): sa, sb = near(pl[a]), near(pl[b]) seen, n = flood(g1, net, sa) hit = any(seen[l][j * NXd + i] for (l, i, j) in sb) print(u' 失败那一刻的图:%s.%s → %s.%s 泛洪 %s(可达 %.1f mm²,起点 %d 格 / 终点 %d 格)' % ( pl[a][0], pl[a][1], pl[b][0], pl[b][1], u'通' if hit else u'不通', n * Gd * Gd / 2.0, len(sa), len(sb))) sys.stdout.flush() def main(): cache = arg('--cache', R.CACHE) d = json.load(io.open(cache, encoding='utf-8')) pads = R.load_pads() bv = R.load_board_vias() segs = [tuple(s) for s in d['segs']] vias = [tuple(v) for v in d['vias']] first = {} for k, s in enumerate(segs): first.setdefault(s[6], k) skip = set(R.PLANE_NETS) | set(R.DP_NETS) if '--starts' in sys.argv: starts_mode(d, pads, bv, segs, vias) return if '--dbg-cmp' in sys.argv: dbg_cmp(arg('--dbg-cmp').split(','), pads, bv, segs, vias) return if '--replay' in sys.argv: nets = arg('--replay-nets', 'EFUSE_EN4,EFUSE_EN5,PRSNT5#,UP_VBUS_DET,NTC1,INT#,PRSNT2#,PRSNT1#,XTAL1,EFUSE_EN3').split(',') replay_mode(d, pads, bv, segs, vias, nets) return if arg('--nets'): nets = arg('--nets').split(',') else: nets = [] for m in d['logs']: n = m.split(':')[0] if not m.startswith('[') and (u'没布通' in m or u'出不来' in m) and n not in skip and n not in nets: nets.append(n) NX, NY, G = RT.NX, RT.NY, RT.G dump_nets = set(arg('--dump', '').split(',')) - {''} dump = {} print(u'缓存 %s:%d 段 / %d 孔;审计 %d 个网(网格 %.3f)' % (os.path.basename(cache), len(segs), len(vias), len(nets), G)) out = {} for net in nets: w = RT.WIDTH.get(net, RT.W_DEFAULT) halfw = w / 2.0 + RT.CLR g, _gt, _o = R.grid_now(pads, halfw, segs, vias, bv) k = g.nid(net) inv = {v: n for n, v in g._id.items()} own = g.own lab = {l: bytearray(NX * NY) for l in RT.LAY} # 0 = 没到过;1..254 = 连通域号 regions = [] seeds = [(l, o) for l in RT.LAY for o in range(NX * NY) if own[l][o] == k] for (l0, o0) in seeds: if lab[l0][o0]: continue rid = len(regions) + 1 if rid > 250: break cnt = 0 bnd = collections.Counter() i0 = j0 = 10 ** 9 i1 = j1 = -1 dq = collections.deque([(l0, o0)]) lab[l0][o0] = rid while dq: l, o = dq.popleft() cnt += 1 j, i = divmod(o, NX) if i < i0: i0 = i if i > i1: i1 = i if j < j0: j0 = j if j > j1: j1 = j a = own[l] for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)): ii, jj = i + di, j + dj if ii < 0 or jj < 0 or ii >= NX or jj >= NY: continue oo = jj * NX + ii v = a[oo] if v != 0 and v != k: bnd[v] += 1 continue if not lab[l][oo]: lab[l][oo] = rid dq.append((l, oo)) ol = 2 if l == 1 else 1 if not lab[ol][o]: v = own[ol][o] if (v == 0 or v == k) and g.free_via(i, j, net): lab[ol][o] = rid dq.append((ol, o)) regions.append({'cells': cnt, 'bbox': (i0 * G, j0 * G, i1 * G, j1 * G), 'bnd': bnd, 'pads': []}) for p in pads: if p[7] != net or p[6] not in RT.LAY: continue o = RT.gy(p[3]) * NX + RT.gx(p[2]) rid = lab[p[6]][o] name = '%s.%s' % ('J1' if p[0] == R.J1_ALIAS else p[0], p[1]) if rid: regions[rid - 1]['pads'].append(name) live = [r for r in regions if r['pads']] or regions print(u'\n■ %s(线宽 %.2f):%d 个连通域%s' % (net, w, len(live), u'' if len(live) > 1 else u' —— 焊盘都在同一个域里:最终占用图上有路,失败是布线顺序 / 代价造成的')) rec = [] for r in sorted(live, key=lambda r: r['cells']): area = r['cells'] * G * G / 2.0 top = r['bnd'].most_common(8) tops = ['%s×%d%s' % (inv.get(v, u'多网' if v == 255 else '?'), c, '' if inv.get(v) not in first else '@%d' % first[inv.get(v)]) for v, c in top] print(u' 域 %.1f mm²(两层合计/2)外框 x %.1f~%.1f y %.1f~%.1f | 脚 %s' % (area, r['bbox'][0], r['bbox'][2], r['bbox'][1], r['bbox'][3], ' '.join(sorted(set(r['pads']))))) if len(live) > 1 and area < 2000: print(u' 围墙(边界格点数@该网第一段下标/%d):%s' % (len(segs), ' '.join(tops))) rec.append({'area': round(area, 1), 'bbox': [round(t, 2) for t in r['bbox']], 'pads': sorted(set(r['pads'])), 'walls': tops}) out[net] = rec if net in dump_nets: # 两层连通域掩码(给本机画图看墙在哪) dump[net] = {'NX': NX, 'NY': NY, 'G': G, 'L1': base64.b64encode(bytes(lab[1])).decode('ascii'), 'L2': base64.b64encode(bytes(lab[2])).decode('ascii'), 'regions': {str(i + 1): sorted(set(r['pads'])) for i, r in enumerate(regions)}} sys.stdout.flush() json.dump(out, io.open(os.path.join(HERE, '_reach_audit.json'), 'w', encoding='utf-8'), ensure_ascii=False, indent=1) print(u'\n-> _reach_audit.json') if dump: json.dump(dump, io.open(os.path.join(HERE, '_reach_dump.json'), 'w', encoding='utf-8')) print(u'-> _reach_dump.json(%s)' % ' '.join(dump)) if __name__ == '__main__': main()