文件
phonerail/.claude/r2/fan_layout_scan.py

127 行
5.5 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""**在服务器上**比较几套风扇区布局的可布性:每套布局真跑一遍 14 个网的补线,按未完成条数挑。
bash .claude/r2/remote_fan_layout.sh # 入口(本机)
DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/fan_layout_scan.py
## 为什么要扫布局而不是只扫落位
`fan_scan.py` 扫的是**两只座**放哪;这一份扫的是**外围 22 件**怎么排。
2026-09-12 用户把两只座挪成 rot90 竖放之后,外围按 `SAT_GAP=1.5` 重排,
几何全过(真实焊盘三向 0 冲突),14 个网却有 10~11 条布不通 —— 而失败的脚
周围往往只有一两个障碍、出口格点也合法 ⇒ 不是「出不来」,是那一带 L4 真没路了
(L1 整片是 12V 大铜,上不去,等于单层)。
摊开排(加大 `SAT_GAP`)未必更好:链会被拉长(实测 `$6N64` 从 25.2 → 23.8、
`$6N74` 13.5 → 21.6,有增有减)。**光看几何分不出优劣,只能让布线器实测。**
输入 `_base_fan_layouts.json`:`{布局名: {位号: [x, y, rot]}}`(本机 `place_base` 按不同
`--fan-gap` 算好的几套)。输出 `_base_fan_layout_scan.json`:每套的未完成 / 断 / 总长,
以及最优那套的完整段与孔。
"""
import io
import json
import os
import sys
import time
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)
if not os.environ.get('DCDC_ROUTE_SERVER'):
sys.exit(u'⛔ 布线搜索只在 10.5.5.13 上跑(CLAUDE.md):bash .claude/r2/remote_fan_layout.sh')
import router as RT # noqa: E402
import route_base as RB # noqa: E402
import place_base as PL # noqa: E402
import pcb_base as B # noqa: E402
NETS = ['FAN_12V_SW', 'FAN_IN_TACH_RAW', 'FAN_OUT_TACH_RAW', 'FAN_PWM_L',
'FAN_PWM', 'FAN_EN', 'FAN_TACH_IN', 'FAN_TACH_OUT',
'$6N45', '$6N48', '$6N49', '$6N55', '$6N64', '$6N74']
cp = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
layouts = json.load(io.open(os.path.join(HERE, '_base_fan_layouts.json'), encoding='utf-8'))
base_pads = RB.load_pads()
nat = PL.native()
FAN = PL.FAN_REFS
def moved_pads(place):
u"""把这 22 件的焊盘搬到该布局的位置(都是贴片件,没有通孔副本要跟着搬)。"""
out = []
for p in base_pads:
ref = p[0]
if ref not in FAN or ref not in place:
out.append(p)
continue
cx, cy, rot = place[ref]
src = [q for q in nat.get(ref, []) if str(q[0]) == str(p[1])]
if not src:
out.append(p)
continue
_pn, nx, ny, w, h, _net, _sh = src[0]
dx, dy = PL.pad_off(nx, ny, rot, B.BOTTOM)
q = list(p)
q[2], q[3] = round(cx + dx, 4), round(cy + dy, 4)
if int(rot) % 180: # 转 90/270:焊盘的宽高也要换过来
q[4], q[5] = h, w
else:
q[4], q[5] = w, h
out.append(q)
return out
def try_one(place):
pads = moved_pads(place)
segs = [tuple(s) for s in cp['segs']]
vias = [tuple(v) for v in cp['vias']]
board_vias = [tuple(v) for v in cp['board_vias']]
bypad = {}
for p in pads:
if not p[7] or p[7] not in NETS or str(p[1]).endswith(RB.TH_SUFFIX):
continue
bypad.setdefault(p[7], []).append(p)
n0, v0 = len(segs), len(vias)
RT.AUTO_CACHE.clear()
logs = RT.route_groups(bypad, pads, segs, vias, board_vias, {},
priority=tuple(NETS), only=set(NETS))
chk = [q for q in RB.check_pads(pads) if q[7] in set(NETS)]
left = RT.check_connected(chk, segs, vias)
new_s = [list(s) for s in segs[n0:]]
new_v = [list(v) for v in vias[v0:]]
length = sum(((s[3] - s[1]) ** 2 + (s[4] - s[2]) ** 2) ** 0.5 for s in new_s)
return {'logs': list(logs), 'unconnected': list(left), 'segs': new_s, 'vias': new_v,
'len': round(length, 2)}
rows = []
t0 = time.time()
for name in sorted(layouts):
place = {k: tuple(v) for k, v in layouts[name].items()}
try:
r = try_one(place)
except Exception as e: # noqa: BLE001
print(u' %s 跑挂了: %s' % (name, e))
continue
rows.append({'name': name, 'open': len(r['logs']), 'split': len(r['unconnected']),
'len': r['len'], 'segs': r['segs'], 'vias': r['vias'],
'logs': r['logs'], 'unconnected': r['unconnected'], 'place': layouts[name]})
print(u' %-8s 未完成 %2d / 断 %2d / %6.1f mm / %d 段 %d 孔(%.0f s)'
% (name, rows[-1]['open'], rows[-1]['split'], rows[-1]['len'],
len(r['segs']), len(r['vias']), time.time() - t0))
for m in r['logs'][:6]:
print(u' [!] ' + m)
sys.stdout.flush()
rows.sort(key=lambda q: (q['open'], q['split'], q['len']))
print(u'\n排名:')
for q in rows:
print(u' %-8s 未完成 %2d / 断 %2d / %6.1f mm' % (q['name'], q['open'], q['split'], q['len']))
out = {'best': rows[0] if rows else None,
'all': [{k: v for k, v in q.items() if k not in ('segs', 'vias', 'place')} for q in rows]}
json.dump(out, io.open(os.path.join(HERE, '_base_fan_layout_scan.json'), 'w', encoding='utf-8'),
ensure_ascii=False)
print(u'已写 _base_fan_layout_scan.json(best 里带布局 + 完整段 / 孔)')