文件

363 行
18 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- 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()