文件

315 行
15 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""BLADE-PCB_1 布线收尾:在服务器布线结果上**手工**补最后几个网(2026-09-14)。
/c/Python314/python .claude/r2/hand_blade1.py # 合成 + 自检(碎块 / 精确净距),不写缓存
/c/Python314/python .claude/r2/hand_blade1.py --write # 自检过了才写 _blade_routes.json
为什么手工:服务器 6 轮(含挪 4 颗电阻)稳定剩 ~8 个网,全在 0.5 mm 细间距脚被邻网铜封口的地方;
`MODE=repair` 补线又会把已通的 VO_P / +12V_F 当普通线整条重铺(33 对同网过孔叠在一起),不能用。
这里**不做任何搜索**(CLAUDE.md:搜索只在服务器),只放逐段写死的几何,再用两道判据兜底:
① 按铜几何做并查集,逐网数碎块(焊盘 / 线段 / 过孔,同层相交或端点落进焊盘即相连,过孔两层都算);
② `router_check` 的精确净距(0.15)。
底 = `_blade_routes.var3.json`(第六轮 `--nofan --pf`,净距 0 冲突),
并入 `_blade_routes.repaired.json` 里 **SDA 那 6 段 2 孔**(其余网的补线全丢弃,理由见上)。
"""
import io
import json
import math
import os
import sys
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 router as RT # noqa: E402
BASE = os.path.join(HERE, '_blade_routes.blade1_srv6.json') # = 第六轮 v3 的拷贝(var* 会被下一次 remote_blade.sh 覆盖)
REPAIRED = os.path.join(HERE, '_blade_routes.blade1_repair_sda.json') # = 第一次 MODE=repair 的拷贝
PADS = json.load(io.open(os.path.join(HERE, '_blade_pads.json'), encoding='utf-8'))
TAKE_FROM_REPAIR = ('SDA',)
# 手工操作:('del_seg', 层, x0, y0, x1, y1, 网) / ('del_via', x, y, 网) / ('seg', 层, x0, y0, x1, y1, 宽, 网) / ('via', x, y, 网)
OPS = [
# ── $1N147:U2.15 出口被 EFUSE_EN 的换层孔 (43.25, 26.75) 正好堵死 ──
# 孔上移到 (43.25, 27.5)(仍在 L1 那根 x 43.25 竖线上,离 U2.13 的 VO 线 0.29、离 R10.1 0.5),
# U2.15 直线接 R10.1(R10.1 ↔ R9.2 已通)。
('del_via', 43.25, 26.75, 'EFUSE_EN'),
('del_seg', 2, 42.994, 27.285, 43.25, 26.75, 'EFUSE_EN'),
('via', 43.25, 27.5, 'EFUSE_EN'),
('seg', 2, 42.994, 27.285, 43.25, 27.5, 0.3, 'EFUSE_EN'),
('seg', 2, 42.245, 26.784, 44.069, 26.632, 0.15, '$1N147'),
# ── $1N131:U1.8 → R6.1 → C11.1,这片 L4 其实空着,布线器的出线判定没找到口 ──
# U1.8 竖直北上到 35.2,45° 斜进 R6.1(离 C9.1 角 0.23);
# R6.1 往西到 x 64.7 绕开 R6.2 / C10.1(离 $1N126 那根 0.566 宽的竖线 0.18、离 C10.1 0.27),北上再东进 C11.1。
('seg', 2, 66.294, 33.978, 66.294, 35.2, 0.15, '$1N131'),
('seg', 2, 66.294, 35.2, 65.375, 36.119, 0.15, '$1N131'),
('seg', 2, 65.375, 36.126, 64.7, 36.126, 0.15, '$1N131'),
('seg', 2, 64.7, 36.126, 64.7, 39.398, 0.15, '$1N131'),
('seg', 2, 64.7, 39.398, 65.293, 39.398, 0.15, '$1N131'),
# ── SDA:三块 R40.1 | U4.23 | U3.4+J1.B6 ──
# L4:R40.1 先东后北进 U4.23(直斜会蹭 U4.24 焊盘角 0.09);
# 在这段横线上 (15.6, 25.832) 打孔上 L1,L1 沿 x 15.6 南下到 y 20.25(西让 +3V3 竖线 0.58、东让 SCL 孔 0.78),
# 东去接服务器补线那根 SDA L1 横线的拐点 (20.5, 20.25)(北让 INA_ALERT 孔 0.38)。
('seg', 2, 14.849, 25.832, 15.872, 25.832, 0.15, 'SDA'),
('seg', 2, 15.872, 25.832, 15.872, 27.457, 0.15, 'SDA'),
('via', 15.6, 25.832, 'SDA'),
('seg', 1, 15.6, 25.832, 15.6, 20.25, 0.15, 'SDA'),
('seg', 1, 15.6, 20.25, 20.5, 20.25, 0.15, 'SDA'),
# ── EDL_EN:U4.7 南面被 KEY_VDN 贴着、北面夹在 KEY_VUP 孔 (18.75,35.5) 和 VBUS_EN 孔 (20.0,35.0) 之间 ──
# L4 从脚北端微斜到 x 19.3 北上(离 KEY_VUP 孔 0.175、离 VBUS_EN 线 0.196),y 36.144 东去 R63.1(U6.6 已与之相连)。
('seg', 2, 19.121, 33.503, 19.121, 34.45, 0.15, 'EDL_EN'),
('seg', 2, 19.121, 34.45, 19.3, 34.63, 0.15, 'EDL_EN'),
('seg', 2, 19.3, 34.63, 19.3, 36.144, 0.15, 'EDL_EN'),
('seg', 2, 19.3, 36.144, 24.511, 36.144, 0.15, 'EDL_EN'),
# ── $1N164 顺手收直:U2.3 → 孔 → L1 → 孔 → R16.2 这一绕,第二个孔 (38.25, 26.75) 正好堵在 U2.2 出口 ──
# 改成 L4 一根直线 U2.3 引出端 → R16.2(离 R16.1 0.43、离 U2.2 0.2),两孔一段 L1 删掉。
('del_seg', 2, 38.497, 27.285, 38.25, 26.75, '$1N164'),
('del_via', 38.25, 26.75, '$1N164'),
('del_seg', 1, 37.5, 26.75, 38.25, 26.75, '$1N164'),
('del_via', 37.5, 26.75, '$1N164'),
('del_seg', 2, 37.033, 27.112, 37.5, 26.75, '$1N164'),
('seg', 2, 38.497, 27.285, 37.033, 27.112, 0.3, '$1N164'),
# ── EFUSE_PG:R15.1 | U2.2 | U4.16 ──
# L4:U2.2 往西到 x 38.3 南下进 R15.1;这根竖线上 (38.3, 25.8) 打孔上 L1(北让 EFUSE_EN 那根 y 25.0 的 L1 横线 0.35)。
# L1:x 36.9 北上到 29.25(西让 EFUSE_FLT 孔、东让 $1N165 孔 0.225)→ 西去 26.8(夹在 BUCK_PG y 28.25 与 $1N165 y 30.25 之间)
# → 北上到 30.25 → 西去 20.422(夹在 +3V3 y 29.5 与 KEY_VDN y 31.0 之间),打孔下 L4 穿 U4 肚子接 U4.16(离 +3V3 孔 0.3)。
('seg', 2, 39.246, 26.784, 38.3, 26.784, 0.15, 'EFUSE_PG'),
('seg', 2, 38.3, 26.784, 38.3, 24.864, 0.15, 'EFUSE_PG'),
('via', 38.3, 25.8, 'EFUSE_PG'),
('seg', 1, 38.3, 25.8, 36.9, 25.8, 0.15, 'EFUSE_PG'),
('seg', 1, 36.9, 25.8, 36.9, 29.25, 0.15, 'EFUSE_PG'),
('seg', 1, 36.9, 29.25, 26.8, 29.25, 0.15, 'EFUSE_PG'),
('seg', 1, 26.8, 29.25, 26.8, 30.25, 0.15, 'EFUSE_PG'),
('seg', 1, 26.8, 30.25, 20.422, 30.25, 0.15, 'EFUSE_PG'),
('via', 20.422, 30.25, 'EFUSE_PG'),
('seg', 2, 20.422, 30.25, 20.422, 27.457, 0.15, 'EFUSE_PG'),
# ── VO_P:C65.2(INA226 VBUS 采样的去耦)原靠 L3 西南腿打孔、从来没落下孔;腿已拆(pcb_pour.VO_PTS 的说明)──
# L4 往西到 x 24.75(离 $1N165 的 Kelvin 孔 (24.0, 21.25) 0.35),北上到 U3.9 高度,西接 U3.8/U3.9 短接线。
# 线宽 0.3 = VO_P 所属 PWR4A 规则的下限(0.2 会报「物理错误:线宽」);U3.8/U3.9 那根短接线同理从 0.2 换成 0.3。
('seg', 2, 25.781, 21.298, 24.75, 21.298, 0.3, 'VO_P'),
('seg', 2, 24.75, 21.298, 24.75, 22.233, 0.3, 'VO_P'),
('seg', 2, 24.75, 22.233, 24.107, 22.233, 0.3, 'VO_P'),
('del_seg', 2, 24.107, 22.733, 24.107, 22.233, 'VO_P'),
('seg', 2, 24.107, 22.733, 24.107, 22.233, 0.3, 'VO_P'),
# ── +5V:两根金手指的 launch 在 L4 上北上出 +12V 横带(顶 18.0),打孔进 L3 的 +5V 岛(pcb_pour.V5_PTS)──
# A19 (39.0) 直上;A32 (52.0) 在 +12V 东腿(x ≥ 49.5)上方,上到 19.2 再西去 48.3 才落孔。U5 那头原有两个孔直接在岛里。
('seg', 2, 39.0, 11.75, 39.0, 19.2, 0.4, '+5V'),
('via', 39.0, 19.2, '+5V'),
('seg', 2, 52.0, 11.75, 52.0, 19.2, 0.4, '+5V'),
('seg', 2, 52.0, 19.2, 48.3, 19.2, 0.4, '+5V'),
('via', 48.3, 19.2, '+5V'),
# ── 70×58(2026-09-14 用户要求):J3 顶到 60.1 越出新板边,挪到 (33.07, 53.93),两根信号重接 ──
# $3N45:J3.2 北上到 54.3,斜进原换层孔 (31.75, 55.5)(离 KEY_VDN 孔 0.43、离 J3.3 安装脚 0.45)。
# $3N44:J3.1 沿 y 52.481 西去接原 x 28.5 竖线(在 C60.2 上方 1.2、离 H4 孔 1.3),旧的两孔一段 L1 删掉。
('del_seg', 2, 31.994, 55.7, 31.75, 55.5, '$3N45'),
('seg', 2, 33.693, 52.481, 33.693, 54.3, 0.15, '$3N45'),
('seg', 2, 33.693, 54.3, 31.75, 55.5, 0.15, '$3N45'),
('del_seg', 2, 30.744, 55.7, 30.0, 54.75, '$3N44'),
('del_via', 30.0, 54.75, '$3N44'),
('del_seg', 1, 30.0, 54.75, 28.5, 54.75, '$3N44'),
('del_via', 28.5, 54.75, '$3N44'),
('del_seg', 2, 28.5, 54.75, 28.5, 40.75, '$3N44'),
('seg', 2, 28.5, 52.481, 28.5, 40.75, 0.15, '$3N44'),
('seg', 2, 32.443, 52.481, 28.5, 52.481, 0.15, '$3N44'),
]
def load():
base = json.load(io.open(BASE, encoding='utf-8'))
segs = [list(s) for s in base['segs']]
vias = [list(v) for v in base['vias']]
rep = json.load(io.open(REPAIRED, encoding='utf-8'))
sa = set(tuple(s) for s in segs)
va = set(tuple(v) for v in vias)
segs += [list(s) for s in rep['segs'] if s[6] in TAKE_FROM_REPAIR and tuple(s) not in sa]
vias += [list(v) for v in rep['vias'] if v[2] in TAKE_FROM_REPAIR and tuple(v) not in va]
return segs, vias
def near(a, b, tol=1e-3):
return abs(a - b) < tol
def apply(segs, vias, ops):
for op in ops:
k = op[0]
if k == 'del_seg':
_, l, x0, y0, x1, y1, n = op
hit = [s for s in segs if s[0] == l and s[6] == n and (
(near(s[1], x0, 2e-3) and near(s[2], y0) and near(s[3], x1) and near(s[4], y1)) or
(near(s[1], x1) and near(s[2], y1) and near(s[3], x0) and near(s[4], y0)))]
assert hit, u'删线没找到: %s' % (op,)
for s in hit:
segs.remove(s)
elif k == 'del_via':
_, x, y, n = op
hit = [v for v in vias if v[2] == n and near(v[0], x) and near(v[1], y)]
assert hit, u'删孔没找到: %s' % (op,)
for v in hit:
vias.remove(v)
elif k == 'seg':
_, l, x0, y0, x1, y1, w, n = op
segs.append([l, x0, y0, x1, y1, w, n])
elif k == 'via':
_, x, y, n = op
vias.append([x, y, n])
else:
raise ValueError(op)
def _pt_seg(px, py, ax, ay, bx, by):
dx, dy = bx - ax, by - ay
L = dx * dx + dy * dy
t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / L))
return math.hypot(px - ax - t * dx, py - ay - t * dy)
def _seg_seg(a, b):
def ccw(p, q, r):
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
p1, p2, p3, p4 = (a[1], a[2]), (a[3], a[4]), (b[1], b[2]), (b[3], b[4])
d1, d2, d3, d4 = ccw(p3, p4, p1), ccw(p3, p4, p2), ccw(p1, p2, p3), ccw(p1, p2, p4)
if ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)):
return 0.0
return min(_pt_seg(p1[0], p1[1], *p3, *p4), _pt_seg(p2[0], p2[1], *p3, *p4),
_pt_seg(p3[0], p3[1], *p1, *p2), _pt_seg(p4[0], p4[1], *p1, *p2))
def _pt_rect(px, py, p):
dx = max(abs(px - p[2]) - p[4] / 2.0, 0.0)
dy = max(abs(py - p[3]) - p[5] / 2.0, 0.0)
return math.hypot(dx, dy)
def pieces(net, segs, vias):
u"""按铜几何算这个网碎成几块,返回 [[成员描述...], ...](只列焊盘)。"""
items = [('pad', p) for p in PADS if p[7] == net and p[0] != 'J1'] + \
[('pad', p) for p in PADS if p[7] == net and p[0] == 'J1']
items += [('seg', s) for s in segs if s[6] == net]
items += [('via', v) for v in vias if v[2] == net]
par = list(range(len(items)))
def f(i):
while par[i] != i:
par[i] = par[par[i]]
i = par[i]
return i
def layers(it):
t, o = it
if t == 'pad':
return {1, 2} if o[6] == 12 else {o[6]}
if t == 'seg':
return {o[0]}
return {1, 2}
def touch(a, b):
if not (layers(a) & layers(b)):
return False
(ta, oa), (tb, ob) = a, b
eps = 0.02
if ta == 'pad' and tb == 'pad':
return False
if ta == 'pad' or tb == 'pad':
p, (t, o) = (oa, b) if ta == 'pad' else (ob, a)
if t == 'via':
return _pt_rect(o[0], o[1], p) < 0.3 + eps
return min(_pt_rect(o[1], o[2], p), _pt_rect(o[3], o[4], p)) < o[5] / 2.0 + eps
if ta == 'via' and tb == 'via':
return math.hypot(oa[0] - ob[0], oa[1] - ob[1]) < 0.6
if ta == 'via' or tb == 'via':
v, s = (oa, ob) if ta == 'via' else (ob, oa)
return _pt_seg(v[0], v[1], s[1], s[2], s[3], s[4]) < 0.3 + s[5] / 2.0 + eps
return _seg_seg(oa, ob) < (oa[5] + ob[5]) / 2.0 + eps
for i in range(len(items)):
for j in range(i + 1, len(items)):
if f(i) != f(j) and touch(items[i], items[j]):
par[f(i)] = f(j)
groups = {}
for i, it in enumerate(items):
groups.setdefault(f(i), []).append(it)
out = []
for g in groups.values():
pads = ['%s.%s' % (o[0], o[1]) for t, o in g if t == 'pad']
if pads:
out.append(pads)
return out
# 靠铜面 / L3 岛成网,不按走线判碎块
PLANE = ('GND', 'VO_P', '+12V', '+5V')
def plane_islands():
import pcb_pour as PP
return {'VO_P': PP.VO_RECTS, '+12V': PP.V12_RECTS, '+5V': PP.V5_RECTS}
def plane_report(segs, vias):
u"""平面网:按走线算出的每一块,都必须至少有一个本网过孔**落在本网 L3 岛里**(离岛边 ≥ 0.3)。"""
isl = plane_islands()
bad = {}
for n, rects in isl.items():
vin = [v for v in vias if v[2] == n and any(r[0] + 0.3 <= v[0] <= r[2] - 0.3 and r[1] + 0.3 <= v[1] <= r[3] - 0.3
for r in rects)]
for g in pieces(n, segs, vias):
# 这一块里有没有岛内过孔:把岛内过孔当成本块成员再算一次块数,不增加 = 在块里
ok = any(len(pieces(n, segs, [v] + [w for w in vias if w[2] == n and w is not v])) and
_in_piece(n, g, v, segs, vias) for v in vin)
if not ok:
bad.setdefault(n, []).append(g)
return bad
def _in_piece(net, g, v, segs, vias):
u"""过孔 v 是否和焊盘组 g 在同一块铜里:把 v 换成一个假焊盘,看它和 g 是否同组。"""
fake = ['VIA', '%.2f,%.2f' % (v[0], v[1]), v[0], v[1], 0.6, 0.6, 12, net, 'RECT']
PADS.append(fake)
try:
for grp in pieces(net, segs, vias):
if fake[0] + '.' + fake[1] in grp:
return any(x in grp for x in g)
finally:
PADS.remove(fake)
return False
def report(segs, vias):
nets = sorted(set(p[7] for p in PADS if p[7] and p[7] not in PLANE))
bad = {}
for n in nets:
g = pieces(n, segs, vias)
# J1 的 launch 由 finger_escape 的引出脚接,焊盘本身在指上:只要不是「J1 自己单独一块」就算通
if len(g) > 1:
bad[n] = g
return bad
def main():
segs, vias = load()
apply(segs, vias, OPS)
bad = report(segs, vias)
pb = plane_report(segs, vias)
print(u'平面网(每块都要有岛内过孔):%s' % (u'全过' if not pb else ''))
for n, gs in sorted(pb.items()):
print(u' [X] %-6s 没接进岛的块: %s' % (n, ' | '.join(','.join(x) for x in gs)))
print(u'段 %d / 孔 %d;碎块网 %d 个' % (len(segs), len(vias), len(bad)))
for n, g in sorted(bad.items()):
print(u' %-10s %d 块: %s' % (n, len(g), ' | '.join(','.join(x) for x in g)))
out = {'segs': segs, 'vias': vias, 'logs': [u'%s: %d 块' % (n, len(g)) for n, g in bad.items()],
'hand': True}
tmp = os.path.join(HERE, '_blade_routes.hand.json')
json.dump(out, io.open(tmp, 'w', encoding='utf-8'), ensure_ascii=False)
print(u'已写 %s(router_check 用 --cache 指过来核净距)' % os.path.basename(tmp))
if '--write' in sys.argv:
json.dump(out, io.open(os.path.join(HERE, '_blade_routes.json'), 'w', encoding='utf-8'),
ensure_ascii=False)
print(u'已写 _blade_routes.json')
if __name__ == '__main__':
main()