363 行
18 KiB
Python
363 行
18 KiB
Python
# -*- 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()
|