158 行
6.8 KiB
Python
158 行
6.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""**在服务器上**扫「两只风扇座放哪里 + 这四个网能不能布通」—— 位置和可布性一起挑。
|
||
|
||
bash .claude/r2/remote_repair.sh --scan # 入口(本机)
|
||
DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/fan_scan.py # 服务器上实际执行的
|
||
|
||
## 为什么非扫不可
|
||
|
||
J26 / J27 挪到顶面之后落在西北角,而那一带 **L1 整片是 `+12V` 大铜**
|
||
(`pcb_base.l1_12v_rects()` 主块 x 0.5~27.8 / y 0.5~97.5)⇒ 过孔上不去,
|
||
等于**单层(L4)布线**,而 A6 的 L4 挤满了风扇驱动的那一窝件。
|
||
于是「几何上放得下」不等于「线接得上」:第一版落位 (7.5, 81.1) / (11.5, 75.1)
|
||
几何全过,补线却有 4 条没布通。
|
||
|
||
⇒ 候选落位(`_base_fan_cands.json`,本机按几何 + 焊盘净距筛出来的)逐个真跑一遍补线,
|
||
按「未完成条数 → 总铜长」挑。单轮补线在 PyPy 上约 1.2 s,几十个候选一分钟就扫完。
|
||
|
||
输出 `_base_fan_scan.json`:每个候选的未完成数 / 总长,以及最优那个的完整段与孔。
|
||
"""
|
||
import copy
|
||
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_repair.sh --scan')
|
||
|
||
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_W=0.15`:见 repair_base.py —— 0402 两脚净距 0.30,默认宽度下 A* 找不到出口格点。
|
||
if os.environ.get('FAN_W'):
|
||
_w = float(os.environ['FAN_W'])
|
||
for _n in NETS:
|
||
RT.WIDTH[_n] = _w
|
||
_orig_rule_min = RT.rule_min
|
||
RT.rule_min = lambda net, auto: (_w if net.startswith('FAN_') else _orig_rule_min(net, auto))
|
||
print(u'线宽覆盖:FAN_* = %.2f mm' % _w)
|
||
LIMIT = int(os.environ.get('SCAN_LIMIT') or 0)
|
||
cp = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
|
||
cands = json.load(io.open(os.path.join(HERE, '_base_fan_cands.json'), encoding='utf-8'))
|
||
if LIMIT:
|
||
cands = cands[:LIMIT]
|
||
base_pads = RB.load_pads()
|
||
nat = PL.native()
|
||
|
||
|
||
def moved_pads(place):
|
||
u"""把 J26 / J27 的焊盘搬到候选位置(只改这两件的脚,别的一只不动)。
|
||
|
||
⚠ 通孔盘在 `route_base.load_pads()` 里已经被拆成 L4 可布 + L1 障碍两份
|
||
(`TH_SUFFIX`),两份都要搬,否则 L1 那份还挡在老地方。
|
||
"""
|
||
out = []
|
||
for p in base_pads:
|
||
ref = p[0]
|
||
if ref not in ('J26', 'J27'):
|
||
out.append(p)
|
||
continue
|
||
cx, cy, rot = place[ref]
|
||
pn = str(p[1])[:-len(RB.TH_SUFFIX)] if str(p[1]).endswith(RB.TH_SUFFIX) else str(p[1])
|
||
src = [q for q in nat[ref] if str(q[0]) == pn]
|
||
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.TOP)
|
||
q = list(p)
|
||
q[2], q[3] = round(cx + dx, 4), round(cy + dy, 4)
|
||
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)}
|
||
|
||
|
||
BODY = (10.2, 5.0) # 2510-4A 本体(`place_base.BODY`)
|
||
|
||
|
||
def overlap(a, b, gap=0.6):
|
||
u"""⛔ 两只座的**本体**不许重叠(顶面同一面)。
|
||
|
||
2026-09-12 踩过:候选表只按「y 差 ≥ 5.6」过滤,J26 钉死在原位那一轮没带这条,
|
||
扫出来的「最优」是 J26=(13.5, 94.5) / J27=(12.0, 94.5) —— 两只座**摞在一起**,
|
||
布线代价当然最低。几何判据缺一条,搜索就会精准地钻这个空子。
|
||
"""
|
||
ax0, ax1 = a[0] - BODY[0] / 2 - gap / 2, a[0] + BODY[0] / 2 + gap / 2
|
||
ay0, ay1 = a[1] - BODY[1] / 2 - gap / 2, a[1] + BODY[1] / 2 + gap / 2
|
||
bx0, bx1 = b[0] - BODY[0] / 2 + gap / 2, b[0] + BODY[0] / 2 - gap / 2
|
||
by0, by1 = b[1] - BODY[1] / 2 + gap / 2, b[1] + BODY[1] / 2 - gap / 2
|
||
return not (ax1 <= bx0 or ax0 >= bx1 or ay1 <= by0 or ay0 >= by1)
|
||
|
||
|
||
rows = []
|
||
skipped = 0
|
||
t0 = time.time()
|
||
for k, (a, b) in enumerate(cands):
|
||
if overlap(a, b):
|
||
skipped += 1
|
||
continue
|
||
place = {'J26': (a[0], a[1], a[2]), 'J27': (b[0], b[1], b[2])}
|
||
try:
|
||
r = try_one(place)
|
||
except Exception as e: # noqa: BLE001
|
||
print(u' [%d] %s / %s 跑挂了: %s' % (k, a, b, e))
|
||
continue
|
||
rows.append({'J26': a, 'J27': b, 'open': len(r['logs']), 'split': len(r['unconnected']),
|
||
'len': r['len'], 'segs': r['segs'], 'vias': r['vias'],
|
||
'logs': r['logs'], 'unconnected': r['unconnected']})
|
||
if (k + 1) % 10 == 0:
|
||
best = min(rows, key=lambda q: (q['open'], q['split'], q['len']))
|
||
print(u' 扫了 %d/%d(%.0f s);目前最好:未完成 %d / 断 %d / %.1f mm J26=%s J27=%s'
|
||
% (k + 1, len(cands), time.time() - t0, best['open'], best['split'],
|
||
best['len'], best['J26'], best['J27']))
|
||
sys.stdout.flush()
|
||
|
||
rows.sort(key=lambda q: (q['open'], q['split'], q['len']))
|
||
print(u'\n扫完 %d 个候选,用时 %.0f s。前 8 名:' % (len(rows), time.time() - t0))
|
||
for q in rows[:8]:
|
||
print(u' 未完成 %d / 断 %d / %6.1f mm J26=%s J27=%s'
|
||
% (q['open'], q['split'], q['len'], q['J26'], q['J27']))
|
||
out = {'best': rows[0] if rows else None,
|
||
'top': [{k: v for k, v in q.items() if k not in ('segs', 'vias')} for q in rows[:30]],
|
||
'tried': len(rows)}
|
||
json.dump(out, io.open(os.path.join(HERE, '_base_fan_scan.json'), 'w', encoding='utf-8'),
|
||
ensure_ascii=False)
|
||
print(u'已写 _base_fan_scan.json(best 里带完整段 / 孔)')
|