文件

490 行
22 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""下板 PCB1:**还没布通的每一条连接,卡在哪、差多少**(纯本地,不碰桥接)。
/c/Python314/python .claude/pcb1_unrouted.py # 全部
/c/Python314/python .claude/pcb1_unrouted.py --net SDA SCL # 只看这几个网络
/c/Python314/python .claude/pcb1_unrouted.py --json out.json
## 它回答的是布线器日志回答不了的问题
`pcb1_route_sig.py` 报「[!!] X 布不通:(x,y) -> (x,y)」时,看不出是
**几何上根本过不去**(要挪件 / 改层)还是**被先布的线挤掉了**(多跑几轮 --ripup)。
这里对每一条还欠的连接,在两端包围盒开一个窗,按引擎同一套判据
(`Field` / `route_grid`,CLR 0.20 + 半线宽 + QERR)从宽到窄试线宽、从粗到细试栅格:
· 0.1 栅格 + 默认线宽就通 ⇒ **E 单发拥堵**(布线顺序 / 窗口 / 拆线额度的事)
· 0.05 栅格才通 ⇒ 栅格量化余量不够(§26.22b 那一类,差的是 QERR 那 0.07)
· 只有更细的线才通 ⇒ 几何缺口:报**瓶颈点**、两侧是谁、缝多宽、差多少 mm
· 0.03 的线都过不去 ⇒ 被围死:报两端焊盘 1.5 mm 内的全部异网铜
⚠️ **只读本地缓存**:`_pcb1_place_now.json`(板上铜快照)/ `pcb1-parts.json` /
`_pcb1_poured.json` / `_pcb1_polypads.json`。`eda.run` 在 import 之前就被换成抛异常的桩,
任何路径想碰桥接都会当场炸,不会画到别的板上去。
"""
import io
import json
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import eda # noqa: E402
def _blocked(*a, **k):
raise RuntimeError(u'pcb1_unrouted.py 是纯本地工具,禁止碰桥接')
eda.run = _blocked
eda.port = _blocked
import numpy as np # noqa: E402
import pcb1_route_sig as R # noqa: E402
E, D, NL = R.E, R.D, R.NL
L_TOP, L_BOT = R.L_TOP, R.L_BOT
CLR = R.CLR
NEED_W = 0.20 # 一般信号线宽
PAD = 2.0 # 搜索窗比两端包围盒大多少(mm)
LADDER = (0.15, 0.10, 0.06, 0.03) # 默认线宽之后依次试的线宽
# ══════════════════ 小工具 ══════════════════
def shbox(s):
if s[0] == 'rect':
return (s[1] - s[3], s[2] - s[4], s[1] + s[3], s[2] + s[4])
if s[0] == 'via':
return (s[1] - s[3], s[2] - s[3], s[1] + s[3], s[2] + s[3])
h = s[5]
return (min(s[1], s[3]) - h, min(s[2], s[4]) - h, max(s[1], s[3]) + h, max(s[2], s[4]) + h)
def bbox(shapes, pad):
bs = [shbox(s) for s in shapes]
return (min(b[0] for b in bs) - pad, min(b[1] for b in bs) - pad,
max(b[2] for b in bs) + pad, max(b[3] for b in bs) + pad)
def sh_dist(s, x, y, lay):
u"""点到形状的净距(负 = 在里面)。不在这一层返回 None。"""
if lay not in s[-1]:
return None
if s[0] == 'rect':
return D.seg_rect((x, y), (x, y), s[1], s[2], s[3], s[4])
if s[0] == 'via':
return math.hypot(x - s[1], y - s[2]) - s[3]
return D.seg_pt(s[1], s[2], s[3], s[4], x, y) - s[5]
def describe(s):
if s[0] == 'rect':
return u'盘/铜 (%.2f,%.2f) %.2fx%.2f' % (s[1], s[2], s[3] * 2, s[4] * 2)
if s[0] == 'via':
return u'过孔 (%.2f,%.2f)' % (s[1], s[2])
return u'线 (%.2f,%.2f)-(%.2f,%.2f) w%.2f L%s' % (s[1], s[2], s[3], s[4], s[5] * 2, s[6][0])
def near(obstacles, x, y, lay, net, r, pads_by_xy):
rows = []
for s, n in obstacles:
if n == net:
continue
d = sh_dist(s, x, y, lay)
if d is None or d > r:
continue
what = describe(s)
if s[0] == 'rect':
tag = pads_by_xy.get((round(s[1], 2), round(s[2], 2)))
if tag:
what = tag + u' ' + what
rows.append((round(d, 3), n or u'(空)', what))
rows.sort()
return rows
def frag_desc(f):
tags = [q['tag'] for q in f['pads']]
return u'%s%s +%d孔 +%d线' % (' '.join(tags[:4]), u'…' if len(tags) > 4 else '',
len(f['vias']), len(f['lines']))
def frag_pt(f):
xs, ys = [], []
for q in f['pads']:
xs.append(q['x']); ys.append(q['y'])
for v in f['vias']:
xs.append(v['x']); ys.append(v['y'])
for l in f['lines']:
xs += [l['x0'], l['x1']]; ys += [l['y0'], l['y1']]
return sum(xs) / len(xs), sum(ys) / len(ys)
def frag_pt_safe(f):
u"""只由铺铜构成的碎片(GND 的内层平面)没有焊盘 / 孔 / 线 —— 拿铺铜格的中心顶上。"""
g = f.get('pour_xy')
return g if g else (0.0, 0.0)
def frag_gap(fa, fb):
u"""两个碎片的铜之间最近的距离(按焊盘中心 / 孔心 / 线端点粗算)。"""
def pts(f):
out = [(q['x'], q['y']) for q in f['pads']] + [(v['x'], v['y']) for v in f['vias']]
for l in f['lines']:
out += [(l['x0'], l['y0']), (l['x1'], l['y1'])]
return out
A, B = pts(fa), pts(fb)
if not A:
A = [frag_pt_safe(fa)]
if not B:
B = [frag_pt_safe(fb)]
best = None
for ax, ay in A:
for bx, by in B:
d = math.hypot(ax - bx, ay - by)
if best is None or d < best[0]:
best = (d, (ax, ay), (bx, by))
return best
def is_stubby(f):
u"""「一个焊盘 + 只有自己的出线段」也算孤立焊盘(上一轮把出线段下发了)。"""
if len(f['pads']) != 1 or f['vias']:
return False
q = f['pads'][0]
for l in f['lines']:
if max(math.hypot(l['x0'] - q['x'], l['y0'] - q['y']),
math.hypot(l['x1'] - q['x'], l['y1'] - q['y'])) > 1.2:
return False
return True
# ══════════════════ 一条连接:试栅格 × 线宽 ══════════════════
def probe(net, ca, cb, obstacles, relax, wdef, wmin):
u"""返回 dict(grid, w, cells, field) —— **最宽的线、最粗的栅格**里第一个走得通的组合;
全不通返回 None;窗太大(栅格被放粗到 > 0.1)返回 dict(coarse=g)。"""
shapes = ca.shapes + cb.shapes
x0, y0, x1, y1 = bbox(shapes, PAD)
x0, y0 = max(0.0, x0), max(0.0, y0)
x1, y1 = min(R.BW, x1), min(R.BH, y1)
widths = [wdef] + [w for w in (wmin,) + LADDER if w < wdef - 1e-9]
widths = sorted(set(widths), reverse=True)
fields = {}
for g0 in (0.1, 0.05):
g = g0
while ((x1 - x0) / g + 2) * ((y1 - y0) / g + 2) > E.MAX_CELLS:
g *= 2
if g > g0 + 1e-9:
# 窗太大:照引擎的 'tgt' 模式 —— 只在**小的那一坨**周围开 8 mm 的窗,
# 大的那一坨落在窗里的部分当种子(一条都不在就放弃这一档)
small, big = (cb, ca) if len(cb.shapes) <= len(ca.shapes) else (ca, cb)
bx0, by0, bx1, by1 = bbox(small.shapes, 8.0)
bx0, by0 = max(0.0, bx0), max(0.0, by0)
bx1, by1 = min(R.BW, bx1), min(R.BH, by1)
inside = [s for s in big.shapes
if shbox(s)[2] > bx0 and shbox(s)[0] < bx1 and shbox(s)[3] > by0 and shbox(s)[1] < by1]
if not inside:
if not fields:
return dict(coarse=g)
break
g = g0
while ((bx1 - bx0) / g + 2) * ((by1 - by0) / g + 2) > E.MAX_CELLS:
g *= 2
if g > g0 + 1e-9:
if not fields:
return dict(coarse=g)
break
f = E.Field(obstacles, (bx0, by0, bx1, by1), skip_net=net, grid=g)
f.set_relax(relax)
bigc = E.Comp(inside, big.anchor, center=big.center, layers=big.layers)
ma, mb = (E._mask(f, bigc), E._mask(f, small)) if big is ca else (E._mask(f, small), E._mask(f, bigc))
fields[g] = (f, ma, mb)
continue
f = E.Field(obstacles, (x0, y0, x1, y1), skip_net=net, grid=g)
f.set_relax(relax)
fields[g] = (f, E._mask(f, ca), E._mask(f, cb))
for w in widths:
for g in sorted(fields, reverse=True):
f, src, tgt = fields[g]
cells = E.route_grid(f, src, tgt, w)
if cells is not None:
return dict(grid=g, w=w, cells=cells, field=f, box=(x0, y0, x1, y1))
return None
def path_blockers(res, obstacles, own_keys, net):
u"""在「拆光自己人」之后找到的格路上,把**原本挡在路上的自己人铜**按网络列出来。"""
f, cells = res['field'], res['cells']
need = CLR + res['w'] / 2.0 + f.qerr
pts = [(f.x0 + i * f.g, f.y0 + j * f.g, lay) for i, j, lay in cells]
hit = {}
for (s, n), key in own_keys:
if n == net:
continue
for x, y, lay in pts[::2]:
d = sh_dist(s, x, y, lay)
if d is not None and d < need:
hit.setdefault(n, set()).add(describe(s))
break
return hit
def bottleneck(res):
u"""沿找到的格路找 edt 最小的格:那就是这条连接的瓶颈。"""
f, cells = res['field'], res['cells']
best = None
for i, j, lay in cells:
e = float(f.edt[lay][j, i])
if best is None or e < best[0]:
best = (e, f.x0 + i * f.g, f.y0 + j * f.g, lay)
return best
def escape_status(q, obstacles, bare):
u"""焊盘自己出不出得去:(全铜 0.1 栅格, 全铜 0.025 栅格, 拆光自己人 0.025 栅格)。"""
out = []
for obs, g in ((obstacles, E.GRID), (obstacles, R.ESC_GRID), (bare, R.ESC_GRID)):
r = R.breathes(q, None, obs, NEED_W, grid=g)
out.append(bool(r is not None and r[2] is not None))
return tuple(out)
def via_spot(q, obstacles, g=0.05, win=3.0):
u"""从这个焊盘出发(本层 0.2 线能走的格),最近的**合法孔位**离焊盘多远。
返回 (距离, x, y) 或 None(3 mm 内一个合法孔位都摸不到)。"""
from scipy import ndimage
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)])
lay = q['lay'] if q['lay'] != 12 else L_TOP
ok = f.free(lay, NEED_W)
i, j = f._idx(q['x'], q['y'])
seed = np.zeros_like(ok)
seed[j, i] = True
reach = ndimage.binary_propagation(seed, mask=ok)
cand = reach & f.via_ok()
jj, ii = np.nonzero(cand)
if not len(jj):
return None
xs = f.x0 + ii * f.g
ys = f.y0 + jj * f.g
d = np.hypot(xs - q['x'], ys - q['y'])
k = int(np.argmin(d))
return (round(float(d[k]), 2), round(float(xs[k]), 2), round(float(ys[k]), 2))
# ══════════════════ 分类 ══════════════════
CH_PAGE = {'$2N': 1, '$8N': 2, '$10N': 3, '$12N': 4, '$14N': 5}
def channel_of(net, frags):
for k, n in CH_PAGE.items():
if net.startswith(k):
return n
return None
def signature(frags):
u"""把网络按「接了哪些件的哪些脚」归一化(去掉通道号),好跨五路对齐。"""
sig = set()
for f in frags:
for q in f['pads']:
ref = q['ref']
m = ref.rstrip('0123456789')
digits = ref[len(m):]
if len(digits) == 3:
ref = m + '%' + digits[1:]
sig.add(ref + '.' + q['num'])
return tuple(sorted(sig))
def classify(net, frags, sigs_by_ch, end_escape):
if net in ('USB_DP', 'USB_DM'):
return 'B'
if net in ('SDA', 'SCL', 'INA_ALERT'):
return 'D'
if any(not e[2] for e in end_escape.values()):
return 'A'
if channel_of(net, frags) is not None:
return 'C'
return 'E'
# ══════════════════ 主流程 ══════════════════
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('--'))
outjson = None
if '--json' in argv:
outjson = argv[argv.index('--json') + 1]
pads, vias, lines = R.board(False)
pours = R.pour_rects()
obstacles = R.obstacles_of(pads, vias, lines, pours)
# 「拆光自己人」的障碍表:只留焊盘 / 扇出 / 功率级 / 铺铜(按累计计划的归属表认)
pall, pall_src = R.plan_all()
lown, vown = R.own_of(pall, lines, vias, verbose=False)
bare_l = [l for i, l in enumerate(lines) if not lown[i]]
bare_v = [v for i, v in enumerate(vias) if not vown[i]]
bare = R.obstacles_of(pads, bare_v, bare_l, pours)
own_keys = [((E.sh_seg(l['x0'], l['y0'], l['x1'], l['y1'], l['w'], l['lay']), l['net']), lown[i])
for i, l in enumerate(lines) if lown[i]]
own_keys += [((E.sh_via(v['x'], v['y'], v['r']), v['net']), vown[i])
for i, v in enumerate(vias) if vown[i]]
print(u'布线器自己画的:%d 线 / %d 孔(归属表 %s);拆光它们之后剩 %d 线 / %d 孔' % (
len(lines) - len(bare_l), len(vias) - len(bare_v), pall_src, len(bare_l), len(bare_v)))
pads_by_xy = {}
for q in pads:
pads_by_xy[(round(q['x'], 2), round(q['y'], 2))] = q['tag']
print(u'板上:%d 焊盘 / %d 过孔 / %d 走线(%s)' % (
len(pads), len(vias), len(lines), os.path.basename(R.SNAP)))
js = R.jobs(pads, want, False, skip=())
total = sum(len(f) - 1 for _n, f in js)
print(u'还欠:%d 个网络 / %d 条连接\n' % (len(js), total))
# 五路对齐:同一个签名在几个通道里都欠着
sigs_by_ch = {}
for net, frags in js:
ch = channel_of(net, frags)
if ch is not None:
sigs_by_ch.setdefault(signature(frags), set()).add(ch)
report = []
for net, frags in js:
wdef, wmin = R.wclass(net)
relax = [E.sh_rect(cx, cy, hw, hh, R.pad_layers(q))
for q in pads if q['net'] == net for cx, cy, hw, hh in R.pad_rects(q)]
# 只由铺铜构成的碎片(内层 GND 平面的某一块)没有任何几何可以当种子,
# 布线器自己也连不上它 —— 单独列出来,不参与探路。
pour_only = [f for f in frags if not (f['pads'] or f['vias'] or f['lines'])]
frags = [f for f in frags if f['pads'] or f['vias'] or f['lines']]
if pour_only:
print(u'━━ %s 有 %d 个**纯铺铜**碎片(内层平面被切开的块),布线器接不上,'
u'要看 pcb1_pour / inner 平面' % (net, len(pour_only)))
if len(frags) < 2:
continue
comps = [R.comp_of(f) for f in frags]
for c, f in zip(comps, frags):
c.frag = f
# 和 route_net 一样:从最大的一坨起头,Prim 最近邻
comps.sort(key=lambda c: -len(c.shapes))
merged, rest = [comps[0]], comps[1:]
# 两端焊盘出得出去?
end_escape = {}
for f in frags:
if is_stubby(f):
q = f['pads'][0]
end_escape[q['tag']] = escape_status(q, obstacles, bare)
cls = classify(net, frags, sigs_by_ch, end_escape)
print(u'━━ %s [%s] %d 碎片,线宽 %.2f/%.2f' % (net, cls, len(frags), wdef, wmin))
bytag_all = {q['tag']: q for q in pads}
for tag, (g1, g2, g3) in sorted(end_escape.items()):
vs_full = via_spot(bytag_all[tag], obstacles)
vs_bare = via_spot(bytag_all[tag], bare)
print(u' 端点 %-9s 出线:全铜 0.1 栅格 %s / 0.025 栅格 %s / **拆光自己人** %s%s;'
u'最近合法孔位:全铜 %s / 拆光自己人 %s'
% (tag, u'通' if g1 else u'✘', u'通' if g2 else u'✘', u'通' if g3 else u'✘',
u'' if g3 else u' ← 摆位几何上就出不去',
(u'%.2f mm (%.2f,%.2f)' % vs_full) if vs_full else u'**3 mm 内没有**',
(u'%.2f mm (%.2f,%.2f)' % vs_bare) if vs_bare else u'**3 mm 内没有**'))
conns = []
while rest:
best = None
for m in merged:
for t in rest:
d = math.hypot(m.anchor[0] - t.anchor[0], m.anchor[1] - t.anchor[1])
if best is None or d < best[0]:
best = (d, m, t)
_d, m, t = best
rest.remove(t)
merged.append(t)
gap = frag_gap(m.frag, t.frag)
res = probe(net, m, t, obstacles, relax, wdef, wmin)
res_bare = None
if res is None or 'coarse' in res:
res_bare = probe(net, m, t, bare, relax, wdef, wmin)
row = dict(net=net, cls=cls, a=frag_desc(m.frag), b=frag_desc(t.frag),
a_pt=[round(v, 2) for v in m.anchor], b_pt=[round(v, 2) for v in t.anchor],
gap=round(gap[0], 2), gap_from=[round(v, 2) for v in gap[1]],
gap_to=[round(v, 2) for v in gap[2]])
if res is not None and 'coarse' in res:
row['verdict'] = u'窗太大'
print(u' ? %s ⇢ %s 缝 %.2f mm:跨度太大,0.1 栅格放不下(要放粗到 %.2g),'
u'不在这里判 —— 长网络的事交给 --ripup' % (row['a'], row['b'], gap[0], res['coarse']))
if res_bare is not None and 'coarse' not in res_bare:
print(u' (拆光自己人之后 w%.2f / %.2g 栅格能通)' % (res_bare['w'], res_bare['grid']))
elif res is None and res_bare is not None and 'coarse' not in res_bare and res_bare['w'] >= wmin - 1e-9:
row['verdict'] = u'被自己人挤死'
hit = path_blockers(res_bare, obstacles, own_keys, net)
row['rip'] = {k: sorted(v) for k, v in hit.items()}
print(u' ◆ %s ⇢ %s 缝 %.2f mm:拆光布线器自己画的铜之后 w%.2f / %.2g 栅格就通'
u' ⇒ **要拆的是**:%s'
% (row['a'], row['b'], gap[0], res_bare['w'], res_bare['grid'],
u';'.join(u'%s(%d 段)' % (k, len(v)) for k, v in sorted(hit.items()))
or u'(路上没压到谁,是顺序 / 窗口的事)'))
elif res is None:
row['verdict'] = u'围死'
# 两端 1.5 mm 内的异网铜
blk = {}
for f in (m.frag, t.frag):
for q in f['pads'][:2]:
lay = q['lay'] if q['lay'] != 12 else L_TOP
blk[q['tag']] = near(obstacles, q['x'], q['y'], lay, net, 1.5,
pads_by_xy)[:8]
row['blockers'] = blk
print(u' ✘ %s ⇢ %s 缝 %.2f mm:**0.03 的线都过不去**'
% (row['a'], row['b'], gap[0]))
for tag, rows in blk.items():
print(u' %s 周围 1.5 mm:' % tag)
for d_, n_, w_ in rows:
print(u' %6.3f %-12s %s' % (d_, n_, w_))
else:
e, bx, by, blay = bottleneck(res)
need = CLR + NEED_W / 2.0 + res['field'].qerr
row.update(grid=res['grid'], w=res['w'], bn=[round(bx, 2), round(by, 2), blay],
bn_edt=round(e, 3), short=round(max(0.0, need - e), 3))
if res['w'] >= wmin - 1e-9 and res['grid'] >= 0.1 - 1e-9:
row['verdict'] = u'可达'
print(u' ○ %s ⇢ %s 缝 %.2f mm:0.1 栅格 w%.2f 就通 ⇒ 是布线顺序 / 拆线额度的事'
% (row['a'], row['b'], gap[0], res['w']))
elif res['w'] >= wmin - 1e-9:
row['verdict'] = u'量化'
print(u' △ %s ⇢ %s 缝 %.2f mm:要 %.2g 栅格才通(w%.2f);'
u'瓶颈 (%.2f,%.2f) L%d edt %.3f,按 0.1 栅格差 %.3f'
% (row['a'], row['b'], gap[0], res['grid'], res['w'], bx, by, blay, e,
max(0.0, CLR + NEED_W / 2 + E.QERR - e)))
else:
row['verdict'] = u'几何缺口'
print(u' ✘ %s ⇢ %s 缝 %.2f mm:只有 w%.2f 过得去(%.2g 栅格);'
u'瓶颈 (%.2f,%.2f) L%d edt %.3f,0.20 线要 %.3f ⇒ **差 %.3f mm**'
% (row['a'], row['b'], gap[0], res['w'], res['grid'], bx, by, blay, e,
need, need - e))
rows = near(obstacles, bx, by, blay, net, 1.0, pads_by_xy)[:6]
row['bn_near'] = rows
for d_, n_, w_ in rows:
print(u' %6.3f %-12s %s' % (d_, n_, w_))
conns.append(row)
report.extend(conns)
print()
# 汇总
print(u'\n════ 汇总 ════')
by = {}
for r in report:
by.setdefault(r['cls'], []).append(r)
for k in sorted(by):
print(u' %s 类 %d 条:%s' % (k, len(by[k]), ' '.join(sorted(set(r['net'] for r in by[k])))))
if outjson:
io.open(outjson, 'w', encoding='utf-8').write(
json.dumps(report, ensure_ascii=False, indent=1))
print(u'-> %s' % outjson)
return 0
if __name__ == '__main__':
sys.exit(main())