# -*- 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()