文件
phonerail/.claude/pcb1_escape_audit.py
T
zhou yang和Claude Opus 5 efcba30a0c R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base)
- 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base)
- 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、
  东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base)
- 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面
- 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、
  J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder
  开关(route_base、remote_route.ps1);剩 37 个网待手工补
- 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base);
  已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查)
- 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
2026-09-10 21:02:10 +08:00

189 行
8.1 KiB
Python
原始文件 Blame 文件历史

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