# -*- 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 里带布局 + 完整段 / 孔)')