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

272 行
13 KiB
Python
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""服务器上单独布 UP 对(2026-09-11 混合布线收尾):以板上现有铜为障碍,从预布两端的孔出发,只走 L1/L4。
DCDC_ROUTE_SERVER=1 /root/pypy/bin/pypy3 .claude/r2/up_route.py [--grid 0.125] [--pre-only] [--free-3v3hub] [--centerline]
读 _board_for_up.json(板上快照)+ _base_preroute_0911.json(UP 两端到孔的预布段),写 _up_route.out.json
两种布法:
* 默认 = 照 route_base.route_pairs 的 USBUP:DP 先布,DM 贴着 DP 的带子布。
实测(2026-09-11)DP 总抢 J37 东端槽针与外壳盘之间 ~1.2 mm 的缝,DM 跟不进去就到处换层:DM 10~12 孔,不能用。
* `--centerline` = 整对当一根宽线(外包 0.78 = 两根 0.28 + 对内 0.22)只在 L1 上布,再向两侧各偏 0.25 劈成 DP / DM,
两端各用一小段接回预布孔。A* 保证找到的通道本来就放得下一对;中间不换层,所以两根都只有预布那两颗孔。
对内中心距 0.5:UP_DP–UP_DM 的 net-by-net 规则 USBPAIR 要 0.2 mm(第 17 轮对内 7.3 mil 报过错)。
"""
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
RT.VIA_LAUNCH_BOTH = True
UP = ('UP_DP', 'UP_DM')
EXTRA_FENCE = [] # [(层, (x0, y0, x1, y1))]:中心线额外禁区(fix3.py:J37 北排上方给 J37.19 的 +5V 让路)
PITCH = 0.5 # 对内中心距
if '--pitch' in sys.argv: # 0.5 那版在 U32 端小折角处对内只剩 0.1987(USBPAIR 要 0.2)⇒ 0.52
PITCH = float(sys.argv[sys.argv.index('--pitch') + 1])
L = lambda s: math.hypot(s[3] - s[1], s[4] - s[2]) # noqa: E731
def obstacles(board, pre, keep_up):
if '--pre-only' in sys.argv:
segs = [tuple(s[:7]) for s in pre['segs'] if keep_up or s[6] not in UP]
vias = [tuple(v[:3]) for v in pre['vias'] if keep_up or v[2] not in UP]
else:
segs = [tuple(s[:7]) for s in board['segs'] if s[0] in (1, 2) and s[6] not in UP]
vias = [tuple(v[:3]) for v in board['vias'] if v[2] not in UP]
if keep_up:
segs += [tuple(s[:7]) for s in pre['segs'] if s[6] in UP]
vias += [tuple(v[:3]) for v in pre['vias'] if v[2] in UP]
if '--free-3v3hub' in sys.argv:
# +3V3_HUB 的远程取电线(离 U32 中心 > 8 mm)在 L1 上把 J37 东侧整片围住 —— UP 先布、它最后再补
u32 = (51.5, 38.6)
far = lambda x, y: math.hypot(x - u32[0], y - u32[1]) > 8.0 # noqa: E731
segs = [s for s in segs if not (s[6] == '+3V3_HUB' and far(s[1], s[2]) and far(s[3], s[4]))]
vias = [v for v in vias if not (v[2] == '+3V3_HUB' and far(v[0], v[1]))]
return segs, vias
def route_follow(pads, segs, vias, pre, logs):
u"""默认布法:DP 先、DM 贴着(照 route_pairs 的 USBUP)。"""
def virt(net):
vs = [v for v in pre['vias'] if v[2] == net]
return [['UP', 'fan:%s%d' % (net, k), v[0], v[1], 0.3, 0.3, 1, net, 'VIA'] for k, v in enumerate(vs)]
dp, dm = R.PAIRS['USBUP']
pdp, pdm = virt(dp), virt(dm)
fence = R.u32_fence(pads)
lp = {1: [(0.0, 0.0, R.B.BW, R.B.BH)], 2: [RT.pad_rect(q, 3.0) for q in pdp + pdm]}
s, v = R.route_one(dp, pdp, R.DIFF_W, pads, segs, vias, [], logs, pen_pads=pdm, fence=fence, hug=lp)
band = {1: [], 2: []}
for q in s:
band[q[0]].append(R._seg_box(q, R.HUG_BAND))
for (vx, vy, _n) in v:
for l in RT.LAY:
band[l].append((vx - 1.2, vy - 1.2, vx + 1.2, vy + 1.2))
for q in pdm:
r = RT.pad_rect(q, R.HUG_PAD)
for l in RT.LAY:
band[l].append(r)
s2, v2 = R.route_one(dm, pdm, R.DIFF_W, pads, segs + s, vias + v, [], logs, hug=band, fence=fence)
return list(s) + list(s2), list(v) + list(v2)
def _chain(segs, a, b):
u"""把一堆首尾相接的线段串成 a → b 的折线(点列)。"""
pts = [a]
rest = [((s[1], s[2]), (s[3], s[4])) for s in segs]
cur = a
while rest:
k = min(range(len(rest)), key=lambda i: min(math.hypot(rest[i][0][0] - cur[0], rest[i][0][1] - cur[1]),
math.hypot(rest[i][1][0] - cur[0], rest[i][1][1] - cur[1])))
p, q = rest.pop(k)
if math.hypot(q[0] - cur[0], q[1] - cur[1]) < math.hypot(p[0] - cur[0], p[1] - cur[1]):
p, q = q, p
if math.hypot(p[0] - cur[0], p[1] - cur[1]) > 0.02:
pts.append(p)
pts.append(q)
cur = q
if math.hypot(cur[0] - b[0], cur[1] - b[1]) > 0.02:
pts.append(b)
out = [pts[0]] # 去掉共线的中间点
for i in range(1, len(pts) - 1):
(x0, y0), (x1, y1), (x2, y2) = out[-1], pts[i], pts[i + 1]
if abs((x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1)) > 1e-6 and math.hypot(x1 - x0, y1 - y0) > 1e-6:
out.append(pts[i])
out.append(pts[-1])
return out
def _offset(pts, d):
u"""折线向左侧(行进方向左手)偏 d;拐角取两条偏移线的交点。"""
dirs = []
for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
n = math.hypot(x1 - x0, y1 - y0)
dirs.append(((x1 - x0) / n, (y1 - y0) / n))
nrm = [(-dy, dx) for dx, dy in dirs]
out = [(pts[0][0] + d * nrm[0][0], pts[0][1] + d * nrm[0][1])]
for i in range(1, len(pts) - 1):
(ax, ay), (bx, by) = nrm[i - 1], nrm[i]
(ux, uy), (vx, vy) = dirs[i - 1], dirs[i]
p1 = (pts[i][0] + d * ax, pts[i][1] + d * ay)
p2 = (pts[i][0] + d * bx, pts[i][1] + d * by)
den = ux * vy - uy * vx
if abs(den) < 1e-9:
out.append(p1)
continue
t = ((p2[0] - p1[0]) * vy - (p2[1] - p1[1]) * vx) / den
out.append((p1[0] + t * ux, p1[1] + t * uy))
out.append((pts[-1][0] + d * nrm[-1][0], pts[-1][1] + d * nrm[-1][1]))
return out
def route_centerline(pads, segs, vias, pre, logs):
dp, dm = R.PAIRS['USBUP']
vdp = [v for v in pre['vias'] if v[2] == dp]
vdm = [v for v in pre['vias'] if v[2] == dm]
# 按 x 分端:U32 侧(西)/ J23 侧(东)
u_dp, j_dp = sorted(vdp, key=lambda v: v[0])
u_dm, j_dm = sorted(vdm, key=lambda v: v[0])
c0 = ((u_dp[0] + u_dm[0]) / 2, (u_dp[1] + u_dm[1]) / 2)
c1 = ((j_dp[0] + j_dm[0]) / 2, (j_dp[1] + j_dm[1]) / 2)
print(u'中心线落点 U32 侧 (%.3f, %.3f) / J23 侧 (%.3f, %.3f)' % (c0 + c1))
net = '#UP'
ps = [['UP', 'fan:c0', c0[0], c0[1], 0.3, 0.3, 1, net], ['UP', 'fan:c1', c1[0], c1[1], 0.3, 0.3, 1, net]]
w = PITCH + R.DIFF_W
def astar_l1(g, starts, goals, nn, halfw): # 照 route_base._astar_l4:只 L1、不许换层
s1 = [s for s in starts if s[0] == 1]
g1 = [q for q in goals if q[0] == 1]
vc = RT.VIA_COST
RT.VIA_COST = 1e9
try:
path = R._orig_astar(g, s1, g1, nn, halfw)
finally:
RT.VIA_COST = vc
if path and any(a[0] != 1 for a in path):
return None
return path
old = RT.astar
RT.astar = astar_l1
try:
s, v = R.route_one(net, ps, w, pads, segs, vias, [], logs, fence=list(R.u32_fence(pads)) + list(EXTRA_FENCE))
finally:
RT.astar = old
if not s or v:
logs.append(u'中心线没布通(段 %d / 孔 %d)' % (len(s), len(v)))
return [], []
pts = _chain([q for q in s if q[0] == 1], c0, c1)
left, right = _offset(pts, PITCH / 2), _offset(pts, -PITCH / 2)
d = lambda p, q: math.hypot(p[0] - q[0], p[1] - q[1]) # noqa: E731
# DP 在哪一侧:两端都要对得上,否则这对线在中间得交叉
end_ok_left = d(left[-1], j_dp) + d(right[-1], j_dm) < d(left[-1], j_dm) + d(right[-1], j_dp)
start_ok_left = d(left[0], u_dp) + d(right[0], u_dm) < d(left[0], u_dm) + d(right[0], u_dp)
if end_ok_left != start_ok_left:
logs.append(u'两端的 DP / DM 左右不一致(中心线在中间要交叉),这版不能用')
pdp, pdm = (left, right) if end_ok_left else (right, left)
out = []
for net_, pl, a, b in ((dp, pdp, u_dp, j_dp), (dm, pdm, u_dm, j_dm)):
full = [(a[0], a[1])] + pl + [(b[0], b[1])]
for p, q in zip(full, full[1:]):
if d(p, q) > 1e-4:
out.append((1, round(p[0], 4), round(p[1], 4), round(q[0], 4), round(q[1], 4), R.DIFF_W, net_))
return out, []
def check(new_s, segs, vias, pads):
u"""新铜对异网:线-线 / 线-孔 / 线-盘 最小净距;对内最小净距。"""
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 samples(s, step=0.05):
n = max(2, int(L(s) / step))
return [(s[1] + (s[3] - s[1]) * k / n, s[2] + (s[4] - s[2]) * k / n) for k in range(n + 1)]
worst, pair = (9.0, ''), 9.0
for s in new_s:
pts = samples(s)
bx0, by0, bx1, by1 = min(s[1], s[3]) - 2, min(s[2], s[4]) - 2, max(s[1], s[3]) + 2, max(s[2], s[4]) + 2
for t in segs:
if t[0] != s[0] or t[6] in UP or max(t[1], t[3]) < bx0 or min(t[1], t[3]) > bx1 or \
max(t[2], t[4]) < by0 or min(t[2], t[4]) > by1:
continue
g = min(pd(x, y, t) for x, y in pts) - s[5] / 2 - t[5] / 2
if g < worst[0]:
worst = (g, u'线-线 %s' % t[6])
for v in vias:
if v[2] in UP or not (bx0 <= v[0] <= bx1 and by0 <= v[1] <= by1):
continue
g = min(math.hypot(v[0] - x, v[1] - y) for x, y in pts) - s[5] / 2 - 0.305
if g < worst[0]:
worst = (g, u'线-孔 %s' % v[2])
for q in pads:
if q[7] in UP or q[6] not in (s[0], 12) or not (bx0 <= q[2] <= bx1 and by0 <= q[3] <= by1):
continue
g = min(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)) for x, y in pts) - s[5] / 2
if g < worst[0]:
worst = (g, u'线-盘 %s.%s' % (q[0], q[1]))
for t in new_s:
if t[6] != s[6] and t[0] == s[0]:
pair = min(pair, min(pd(x, y, t) for x, y in pts) - s[5] / 2 - t[5] / 2)
return worst, pair
def main():
board = json.load(io.open(os.path.join(HERE, '_board_for_up.json'), encoding='utf-8'))
pre = json.load(io.open(os.path.join(HERE, '_base_preroute_0911.json'), encoding='utf-8'))
centre = '--centerline' in sys.argv
segs, vias = obstacles(board, pre, keep_up=not centre)
pads = R.load_pads()
print(u'模式 %s;障碍:线 %d / 孔 %d / 焊盘 %d;网格 %.3f' % (u'中心线' if centre else u'DP 先 DM 跟', len(segs), len(vias),
len(pads), RT.G))
logs = []
pad_pen, layer_dir = RT.PAD_PEN, RT.LAYER_DIR
RT.PAD_PEN = R.DP_PAD_PEN
RT.LAYER_DIR = {}
try:
new_s, new_v = (route_centerline if centre else route_follow)(pads, segs, vias, pre, logs)
finally:
RT.PAD_PEN, RT.LAYER_DIR = pad_pen, layer_dir
obst_s = segs + ([] if not centre else [])
worst, pair = check(new_s, obst_s, vias, pads)
all_s = segs + new_s + ([tuple(s[:7]) for s in pre['segs'] if s[6] in UP] if centre else [])
all_v = vias + new_v + ([tuple(v[:3]) for v in pre['vias'] if v[2] in UP] if centre else [])
pairs_bak = R.RB.PAIRS
R.RB.PAIRS = [p for p in pairs_bak if p[0] == 'USBUP']
try:
ret = R.dp_return_vias(pads, all_s, all_v, [], logs)
finally:
R.RB.PAIRS = pairs_bak
lens = {}
for net in UP:
ss = [q for q in all_s if q[6] == net]
vv = [q for q in all_v if q[2] == net]
lens[net] = sum(L(q) for q in ss)
print(u'%s:新增 %d 段 %.1f mm(含预布共 %.1f mm)L1 %.1f / L4 %.1f,孔共 %d'
% (net, sum(1 for q in new_s if q[6] == net), sum(L(q) for q in new_s if q[6] == net),
lens[net], sum(L(q) for q in ss if q[0] == 1), sum(L(q) for q in ss if q[0] == 2), len(vv)))
print(u'长度差 %.2f mm;新铜对异网最小净距 %.3f(%s,规则 0.15);对内最小净距 %.3f(USBPAIR 0.2)'
% (abs(lens['UP_DP'] - lens['UP_DM']), worst[0], worst[1], pair))
print(u'GND 回流孔 %d 颗;日志 %d 条' % (len(ret), len(logs)))
for m in logs:
print(u' ' + m)
io.open(os.path.join(HERE, '_up_route.out.json'), 'w', encoding='utf-8').write(json.dumps(
{'segs': new_s, 'vias': new_v, 'ret': ret, 'logs': logs, 'grid': RT.G, 'centerline': centre,
'worst': worst, 'pair_clr': pair}, ensure_ascii=False))
if __name__ == '__main__':
main()