文件
phonerail/.claude/r2/route_nets.py
T

191 行
9.4 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""服务器上补指定的断网(2026-09-11 混合布线收尾):以板上快照的全部铜为障碍,只把每个网断开的几块接起来。
DCDC_ROUTE_SERVER=1 /root/pypy/bin/pypy3 .claude/r2/route_nets.py NET1 NET2 ... [--grid 0.125]
读 _board_for_nets.json,写 _route_nets.out.json(按网的新增线 / 孔 + 日志)
做法:按板上铜把本网焊盘分连通块(线-线 / 线-孔 / 线-盘 / 孔-盘 相碰即连,内层线也算)→ 每块挑一只离其它块最近的焊盘当代表 →
route_base.route_one 把代表连成树(router 只走 L1/L4,12V 大铜只挡导线、焊盘里不落孔等照 route_base._build)。
按给定顺序一个一个布,前面布出的铜进后面的障碍。
"""
import io
import json
import math
import os
import sys
if os.environ.get('DCDC_ROUTE_SERVER') != '1':
sys.exit(u'!! 布线运算只在服务器上跑(DCDC_ROUTE_SERVER=1),见 CLAUDE.md')
if '--grid' not in sys.argv:
sys.argv += ['--grid', '0.125']
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
import route_base as R # noqa: E402
RT = R.RT
WIDTH = {'+3V3_HUB': 0.3, '+5V': 0.5, '+3V3': 0.4}
# ⛔ 内层线也要挡过孔(2026-09-11 第一版漏了:只拿 L1/L4 当障碍,补网 A 的孔戳在 EasyEDA 留在 L2/L3 上的线上 ——
# DRC:VDAC1 孔压 L3 的 SDA、+3V3_HUB 孔压 L2 的 NTC2,都是 0 mil 短路)。
# router 只有 L1/L4 两张图;过孔要两层的过孔图都空才准落 ⇒ 沿每条内层线在 L1 的过孔图(ownv)上封一圈,走线图不动。
INNER = [] # [(layer, x1, y1, x2, y2, w, net)],main() 每轮刷新
_orig_grid_now = R.grid_now
def _grid_now(pads, halfw, segs, vias, board_vias):
g, gt, others = _orig_grid_now(pads, halfw, segs, vias, board_vias)
for s in INNER:
r = RT.VIA_D / 2.0 + RT.CLR + s[5] / 2.0
n = max(1, int(L(s) / (RT.G / 2.0)))
for k in range(n + 1):
x = s[1] + (s[3] - s[1]) * k / n
y = s[2] + (s[4] - s[2]) * k / n
g.block_rect(1, x - r, y - r, x + r, y + r, net=s[6], via=True)
return g, gt, others
R.grid_now = _grid_now
L = lambda s: math.hypot(s[3] - s[1], s[4] - s[2]) # noqa: E731
def _pd(px, py, s):
x1, y1, x2, y2 = s[1], s[2], s[3], s[4]
dx, dy = x2 - x1, y2 - y1
L2 = dx * dx + dy * dy
t = 0 if L2 == 0 else max(0, min(1, ((px - x1) * dx + (py - y1) * dy) / L2))
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
def _rd(q, x, y):
return math.hypot(max(q[2] - q[4] / 2 - x, x - q[2] - q[4] / 2, 0), max(q[3] - q[5] / 2 - y, y - q[3] - q[5] / 2, 0))
def components(net, pads, segs, vias):
ps = [q for q in pads if q[7] == net]
ss = [s for s in segs if s[6] == net]
vs = [v for v in vias if v[2] == net]
items = [('p', q) for q in ps] + [('s', s) for s in ss] + [('v', v) for v in vs]
par = list(range(len(items)))
def f(i):
while par[i] != i:
par[i] = par[par[i]]
i = par[i]
return i
def seg_seg(a, b):
return min(_pd(a[1], a[2], b), _pd(a[3], a[4], b), _pd(b[1], b[2], a), _pd(b[3], b[4], a))
def touch(a, b):
(ka, x), (kb, y) = a, b
if ka == 'p' and kb == 'p':
return False
if ka == 'v' and kb == 'v':
return math.hypot(x[0] - y[0], x[1] - y[1]) < 0.61
if ka == 's' and kb == 's':
return x[0] == y[0] and seg_seg(x, y) < (x[5] + y[5]) / 2 + 0.01
if kb == 's':
(ka, x), (kb, y) = b, a
if ka == 's' and kb == 'v':
return _pd(y[0], y[1], x) < x[5] / 2 + 0.3
if ka == 's' and kb == 'p':
n = max(2, int(L(x) / 0.05))
return (y[6] in (x[0], 12)) and min(_rd(y, x[1] + (x[3] - x[1]) * k / n, x[2] + (x[4] - x[2]) * k / n)
for k in range(n + 1)) < x[5] / 2 + 0.01
q, v = (x, y) if ka == 'p' else (y, x)
return _rd(q, v[0], v[1]) < 0.3
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):
if it[0] == 'p':
groups.setdefault(f(i), []).append(it[1])
return list(groups.values())
def reps(groups):
u"""每块挑一只离其它块最近的焊盘当代表。"""
out = []
for k, g in enumerate(groups):
others = [q for j, h in enumerate(groups) if j != k for q in h]
best = min(g, key=lambda q: min(math.hypot(q[2] - o[2], q[3] - o[3]) for o in others))
out.append(best)
return out
def main():
# 参数 NET=宽度 覆盖默认线宽(2026-09-11:+5V 0.5 钻不进 J37.19 南侧 0.74 的缝、CFGSEL0 / +3V3_HUB 出不了 U32 脚间)
for a in sys.argv[1:]:
if '=' in a and not a.startswith('--'):
k, v = a.split('=', 1)
WIDTH[k] = float(v)
# ⚠ 带参数的开关(--cut X、--grid G)后面那个值不是网名(第一版把 VDAC1@x,y 当网名混进来过)
skip = {k + 1 for k, a in enumerate(sys.argv) if a in ('--cut', '--grid')}
nets = [a for k, a in enumerate(sys.argv) if k >= 1 and k not in skip and not a.startswith('--') and '=' not in a
and '@' not in a and not a.replace('.', '').isdigit()]
b = json.load(io.open(os.path.join(HERE, '_board_for_nets.json'), encoding='utf-8'))
segs = [tuple(s[:7]) for s in b['segs']] # 内层线也要参与连通判断(不当 L1/L4 障碍)
vias = [tuple(v[:3]) for v in b['vias']]
if '--drop-a' in sys.argv:
# 先在数据里拿掉这些网在「补网 A」(_route_nets.A.json)里布的铜,再重布(孔戳内层线那批)。
# 板上的删除等新结果本机复核过、下发时再做。EasyEDA 会合并共线段 ⇒ 按「两端 + 中点都落在同层同网同宽的 A 段上」认。
a = json.load(io.open(os.path.join(HERE, '_route_nets.A.json'), encoding='utf-8'))
a_s = [tuple(x) for n in nets for x in a['nets'].get(n, {}).get('segs', [])]
a_v = [tuple(x) for n in nets for x in a['nets'].get(n, {}).get('vias', [])]
def in_a(s):
cand = [t for t in a_s if t[0] == s[0] and t[6] == s[6] and abs(t[5] - s[5]) < 0.01]
return cand and all(any(_pd(x, y, t) < 0.01 for t in cand)
for x, y in ((s[1], s[2]), (s[3], s[4]), ((s[1] + s[3]) / 2, (s[2] + s[4]) / 2)))
n0 = (len(segs), len(vias))
segs = [s for s in segs if not (s[6] in nets and in_a(s))]
vias = [v for v in vias if not (v[2] in nets and any(w[2] == v[2] and math.hypot(w[0] - v[0], w[1] - v[1]) < 0.02
for w in a_v))]
print(u'--drop-a:去掉补网 A 的铜 %d 段 / %d 孔(%s)' % (n0[0] - len(segs), n0[1] - len(vias), ' '.join(nets)))
# --cut NET@x,y(可多次):只拆这颗孔和端点落在它上面的同网线段 —— 局部修补(VDAC1 补网 A 那条 97 mm 路径本身合理,
# 只有两颗孔戳在内层线上;整根重布要绕 273 mm / 25 孔)。被拆开的几块照常「连通块 + 代表 + route_one」接回。
cuts = [sys.argv[k + 1] for k, a in enumerate(sys.argv) if a == '--cut']
for c in cuts:
cn, xy = c.split('@')
cx, cy = (float(t) for t in xy.split(','))
nv0, ns0 = len(vias), len(segs)
vias = [v for v in vias if not (v[2] == cn and math.hypot(v[0] - cx, v[1] - cy) < 0.05)]
segs = [s for s in segs if not (s[6] == cn and (math.hypot(s[1] - cx, s[2] - cy) < 0.05 or
math.hypot(s[3] - cx, s[4] - cy) < 0.05))]
print(u'--cut %s @ (%.3f, %.3f):拆孔 %d、线 %d' % (cn, cx, cy, nv0 - len(vias), ns0 - len(segs)))
if cn not in nets:
nets.append(cn)
pads = R.load_pads()
out, logs = {}, []
for net in nets:
INNER[:] = [s for s in segs if s[0] in (15, 16)]
obst = [s for s in segs if s[0] in (1, 2)]
groups = components(net, pads, segs, vias)
if len(groups) < 2:
print(u'%s:已经连通(%d 块),跳过' % (net, len(groups)))
continue
ps = reps(groups)
w = WIDTH.get(net, 0.2)
lg = []
s, v = R.route_one(net, ps, w, pads, obst, vias, [], lg)
segs += [tuple(x) for x in s]
vias += [tuple(x) for x in v]
after = components(net, pads, segs, vias)
out[net] = {'segs': [list(x) for x in s], 'vias': [list(x) for x in v], 'before': len(groups), 'after': len(after),
'logs': lg}
print(u'%s:%d 块 → %d 块;新增 %d 段 %.1f mm / %d 孔(线宽 %.2f)%s'
% (net, len(groups), len(after), len(s), sum(L(x) for x in s), len(v), w, (u';' + ';'.join(lg)) if lg else ''))
logs += lg
io.open(os.path.join(HERE, '_route_nets.out.json'), 'w', encoding='utf-8').write(json.dumps(
{'nets': out, 'logs': logs, 'grid': RT.G,
# 下发时照这两份删板上旧铜:drop_a = 整网拿掉「补网 A」的铜再重布;cuts = 只拆这几颗孔 + 端点在孔上的线(局部修补)
'drop_a': ([n for n in nets if n not in {c.split('@')[0] for c in cuts}] if '--drop-a' in sys.argv else []),
'cuts': cuts}, ensure_ascii=False))
if __name__ == '__main__':
main()