diff --git a/docs/探索日志.md b/docs/探索日志.md index 7ffa203..53cab4d 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -477,6 +477,40 @@ - **结论**:实时配置定为 `dag2 + 解析 J + gain2.2 + commit2 + safety_partial + replan=empty`:n=512 到达 **50.8%**、碰撞 **1.4%**。浏览器端从「0 到达 + 47% 刹车」恢复到 nc 集可达,plan P50 33.5/37.6 ms;P99 仍 61–66 ms,commit=2 的 40 ms 队列会偶发饥饿,需继续压 P99(K/安全候选数/JS 特征)。 - **下一步**:① Python 测 `safety_try=1`(只认证首选候选)对准确率的影响,若无损则 JS 同步(省多次 collideFree);② K=4/6 的准确率-时延权衡;③ JS `gapMatrix` 同步只算 8 组特征;④ commit=2 异步部署的饥饿率实测。 +## 轮次 28:端点进度选择——时间效率大幅提升(exp: `exp/dagger-deploy`,新历史最佳) + +- **假设**:一致性选择奖励"与上一条计划一致",但失败主因是超时(轮次 13:失败 57% 是超时)。改成直接按候选块末态离目标的距离选(`select='endpoint'`),应提升时间效率与精度。 +- **判据**:time_ok_1p3 显著上升、到达不降、碰撞 ≤3%。 +- **配方**:dag2 + 解析 J,gain2.2,commit=2,replan=empty,safety_partial;n=256 → n=512。选择器用 `goal_err(fut[-1])`(只做一次 K 端点 FK,不额外推理)。 +- **指标(n=256,commit=2)**: + +| 选择器 | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | +|---|---|---|---|---|---| +| consist(轮次 27) | 52.0% | 29.7% | 1.6% | 0.329 | 35.3 | +| **endpoint** | **52.7%** | **37.5%** | 2.3% | **0.210** | 34.6 | +| mix(consist − 0.1·端点mm) | 50.0% | 30.5% | 2.3% | 0.300 | 35.1 | + +- **n=512 复核(endpoint,commit=2)**:**到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285 / 刹停 35.7**(consist n=512:50.8% / 28.9% / 1.4% / 0.343 / 35.9)。 +- **节奏曲线(endpoint,n=256)**:commit=1 → **59.4% / 42.6% / 3.9% / 0.133**;commit=2 → 52.7% / 37.5% / 2.3% / 0.210;commit=3 → 47.7% / 27.0% / 1.2% / 0.245。 +- **负结论**: + - `safety_try=1/2`(只认证前 1/2 条候选):41.4% / 44.9%(全 K 52.0%)——候选兜底对到达贡献 ~10 pp,不能省。 + - K=4:47.3%(K=8 52.0%)——少采样同样损失到达。 + - DLS 兜底半步(全拒时朝目标走一步并认证):52.3%(基线 52.0%),碰撞 1.6→2.7%,无收益;重复多步版本碰撞 45.7%(第二步未认证),已回退为单步且默认关闭。 + - 增益调度(远 2.8/近 2.2):47.3% / time_ok 32.8%,等同 gain2.8 常数;高增益在远处就造成过冲,按距离衰减无效。 +- **浏览器同步**:`select='endpoint'` 进 Runner/bench(`--select endpoint`),冷启动跳过 8 份前瞻(省 ~80 ms P99,冷启动价值评分被端点排序替代)。nc10 bench(--seg 0 --select endpoint):pos ok 4/5 reached、pose ok 2/4,刹车率 32.1%/25.5%(consist 48.1%/31.6%),planP50 31.9/36.5 ms、P99 63.5/67.5 ms。 +- **50 Hz 部署口径实测(Runner 增加 `starveTicks`:ceil(planMs/20) − 规划时队列步数)**: + +| 集合(endpoint, commit=2, --seg 0) | 到达 ok/reached | 刹车率 | planP50/P99 | 饥饿 tick/次规划 | +|---|---|---|---|---| +| near_column_pos (10) | 4/5 | 32.7% | 32.0/62.1 ms | 0.87 | +| near_column_pose (10) | 2/3 | 25.6% | 36.8/68.0 ms | 0.78 | +| route40 (20) | 12/13 | 40.4% | 44.3/68.1 ms | 1.26 | + + - commit=2 的队列只有 40 ms,route 集 planP50 44 ms 已经超过 → 每次规划平均空转 ~1.3 tick(50 Hz 下机械臂会"走两步停一下")。 + - **commit=3(60 ms 队列)可覆盖 P50 44 ms 与多数 P99**:endpoint n=256 → 47.7% / 27.0% / 1.2% / 0.245;gain2.5 反而降到 45.7% / 24.6%。**连续 50 Hz 口径选 commit=3(47.7%),极限准确率选 commit=2(52.1%,偶发空转)**。 +- **结论**:**新历史最佳(50 Hz 节奏,n=512)= 端点选择 + 解析 J + gain2.2 + commit=2 + safety_partial:到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285**。相对本会话开始的历史最佳(42.2% / 29.1% / 2.9% / 0.270)到达 +9.9 pp、时间效率 +6.1 pp。commit=1 上限 59.4%(碰撞 3.9%,超 3% 线,且规划频率不可实时)。 +- **下一步**:① 碰撞 2.0% 的来源分解(端点选择使候选更贴障碍?);② 把 `select=endpoint` 写进查看器默认(Runner 默认仍 consist);③ route 集 safety P50 30 ms 是 planP50 的主项,继续压认证成本(候选去重/按需投影已做,剩余在 collideFree 本身)。 + ## 分支清理(2026-10-20) - `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。 diff --git a/ml/eval/eval_cl.py b/ml/eval/eval_cl.py index 8fbdfee..3d7b5a9 100644 --- a/ml/eval/eval_cl.py +++ b/ml/eval/eval_cl.py @@ -40,9 +40,14 @@ def main(): ap.add_argument('--commit', type=int, default=3) ap.add_argument('--fb', type=float, default=1.8) ap.add_argument('--ki', type=float, default=0.0) + ap.add_argument('--fb_scale', type=float, default=0.0) + ap.add_argument('--fb_near', type=float, default=0.2) + ap.add_argument('--fb_fallback', type=float, default=0.0) ap.add_argument('--kdecay', type=float, default=0.9) ap.add_argument('--kimax', type=float, default=0.5) ap.add_argument('--fb_every', type=int, default=1) + ap.add_argument('--safety_try', type=int, default=0) + ap.add_argument('--sel_w', type=float, default=0.1) ap.add_argument('--select', default='consist') ap.add_argument('--replan', default='r3') ap.add_argument('--safety', type=int, default=1, help='1=安全层+部分前缀;0=关') @@ -63,8 +68,8 @@ def main(): t0 = time.perf_counter() r = run(model, kin, tasks, a.device, K=a.K, safety=bool(a.safety), seed=a.seed, max_sec=a.max_sec, safety_partial=(a.commit if (a.safety and a.partial) else 0), commit_n=a.commit, - select=a.select, replan=a.replan, fb_gain=a.fb, fb_ki=a.ki, fb_decay=a.kdecay, fb_imax=a.kimax, - fb_every=a.fb_every) + select=a.select, replan=a.replan, fb_gain=a.fb, fb_scale=a.fb_scale, fb_near=a.fb_near, fb_fallback=a.fb_fallback, fb_ki=a.ki, fb_decay=a.kdecay, fb_imax=a.kimax, + fb_every=a.fb_every, safety_try=a.safety_try, sel_w=a.sel_w) wall = time.perf_counter() - t0 print('ckpt=%s n=%d K=%d commit=%d fb=%.2f ki=%.2f select=%s replan=%s safety=%d' % (a.ckpt, r['n'], a.K, a.commit, a.fb, a.ki, a.select, a.replan, a.safety)) diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index abef76f..10a60f3 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -153,7 +153,7 @@ class TeacherOracle: def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None, qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross', fb_gain=0.0, fb_scale=0.0, fb_near=0.2, fb_fallback=0.0, safety_partial=0, commit_n=3, collect=None, - fb_exec=0.0, exec_safety=0, fb_ki=0.0, fb_decay=0.9, fb_imax=0.5, fb_every=1): + fb_exec=0.0, exec_safety=0, fb_ki=0.0, fb_decay=0.9, fb_imax=0.5, fb_every=1, safety_try=0, sel_w=0.1): """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头; qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放; select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。 @@ -237,6 +237,10 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6] if select == 'first': score = torch.zeros(B, K, device=device); score[:, 0] = 1 + elif select == 'endpoint': + qa = fut[:, :, -1].reshape(B * K, 6) + e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0)) + score = -e_end.view(B, K) elif cold or select == 'value': # 短前瞻:候选末态的价值 qa = fut[:, :, -1].reshape(B * K, 6); qb = fut[:, :, -2].reshape(B * K, 6) @@ -253,6 +257,10 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, # 2026-10-19(审查 review_closedloop F5):上一次是刹停或 DLS 接管的行没有「已提交计划」, # 与 JS(lastPlan = null → 一致性项缺席、各候选同分 → 取第 0 条)一致:同分,稳定排序取第 0 条 score = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0])) + if select == 'mix': # 一致性 + 端点进度(w=sel_w/mm) + qa = fut[:, :, -1].reshape(B * K, 6) + e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0)) + score = score - sel_w * e_end.view(B, K) order = torch.sort(score, dim=-1, descending=True, stable=True).indices pick = order[:, 0] if select == 'average': # 2026-10-20:候选平均(平滑误差)——所有候选置为 K 条动作的均值 @@ -267,7 +275,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, if safety and safety_partial: # 2026-10-20:允许「部分安全前缀」——执行到第一个不安全步为止,而不是全拒刹停 maxlen = torch.zeros(B, dtype=torch.long, device=device) chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone() - for kk in range(K_eff): + ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff + for kk in range(ntry): ck = inc[ar, order[:, kk], r:r + commit_n] pts = qe[:, None] + torch.cumsum(ck, 1) mids = torch.cat([qe[:, None], pts[:, :-1]], 1) + 0.5 * ck @@ -281,12 +290,20 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, if B == 1 and int(maxlen[0]) >= commit_n: break # 实时单题:首个全安全候选即停(安全层 16 点→2-4 点) msk = (torch.arange(commit_n, device=device)[None] < maxlen[:, None]) commit = torch.where(msk[..., None], chosen, torch.zeros_like(chosen)) + if fb_gain > 0 and fb_fallback > 0: # 全拒时兜底:半步朝目标(须通过间隙认证),避免原地停满整个周期 + fb1 = dls_steps(kin, qe, T, R, n=1) * fb_fallback + ok1 = kin.gaps(qe + fb1[:, 0], 0.5)['gm'].min(-1).values > 0 + fbcom = torch.zeros(B, commit_n, 6, device=device); fbcom[:, 0] = fb1[:, 0] # 只走一步(重复会被第二步未经认证的位移带进碰撞) + mskf = (maxlen == 0) & ok1 + commit = torch.where(mskf[:, None, None], fbcom, commit) + msk = msk | mskf[:, None] brakes += (maxlen == 0).float() pick = chosen_k; committed = maxlen > 0 elif safety: ok_any = torch.zeros(B, dtype=torch.bool, device=device) chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone() - for kk in range(K_eff): + ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff + for kk in range(ntry): ck = inc[ar, order[:, kk], r:r + commit_n] pts = qe[:, None] + torch.cumsum(ck, 1) mids = torch.cat([qe[:, None], pts[:, :-1]], 1) + 0.5 * ck diff --git a/tests/policy_bench.cjs b/tests/policy_bench.cjs index 7ea9ef0..d664c5d 100644 --- a/tests/policy_bench.cjs +++ b/tests/policy_bench.cjs @@ -14,7 +14,7 @@ const fs = require('fs'), path = require('path'); const argv = process.argv.slice(2); const flag = k => argv.includes(k); const opt = k => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : null; }; -const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n', '--fb', '--commit', '--seg']); +const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n', '--fb', '--commit', '--seg', '--proj', '--select']); const pos = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VALOPT.has(argv[i - 1]))); const MODEL = pos[0] || path.join(__dirname, '../webapp/models/traj_policy.onnx'); const SETS = (pos[1] || 'nc,route').split(',').concat(flag('--nc1000') ? ['nc1000'] : []); @@ -66,12 +66,12 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x, const cert = POL.makeCert(S, {}); const out = { model: MODEL, meta: { fp: meta.fp, eval: meta.eval } }; const runSet = async (name, cases, mode) => { - let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0; + let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0, starve = 0; const inf = [], saf = [], look = [], planT = [], planW = [], cq = [], times = [], fails = []; let hits = 0; const why = {}; for (let ci = 0; ci < cases.length; ci++) { const c = cases[ci]; const R = mode === 'pose' ? c.R : null; - const r = new POL.Runner(S, infer, { seed: 1000 + ci, actScale: meta.act_scale || 1, fbGain: +opt('--fb') || 0, commit: Math.max(1, +opt('--commit') || 3), segCert: opt('--seg') !== '0' }); // 2026-10-19:动作尺度随模型 + const r = new POL.Runner(S, infer, { seed: 1000 + ci, actScale: meta.act_scale || 1, fbGain: +opt('--fb') || 0, commit: Math.max(1, +opt('--commit') || 3), segCert: opt('--seg') !== '0', projMax: opt('--proj') === '0' ? 0 : 0.5, select: opt('--select') || 'consist' }); // 2026-10-19:动作尺度随模型 r.reset(c.q0); r.setGoal(c.T, R); let prev = c.q0.slice(), cf = 0, tLock = null; for (let k = 0; k < 500 && !r.done; k++) { @@ -86,14 +86,14 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x, const { e, oe } = r.err(r.q); const rc = r.done && e < 1 && oe < 0.5; if (rc) reached++; if (cf) { colTasks++; colFrames += cf; } if (rc && !cf) { ok++; times.push(r.t); } else fails.push({ ci, err: +e.toFixed(1), oerr: +oe.toFixed(1), colFrames: cf, brakes: r.stats.brakes }); - inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); look.push(...r.stats.lookaheadMs); planT.push(...r.stats.planMs); planW.push(...r.stats.planWarmMs); cq.push(...r.stats.certQ); hits += r.stats.certHit; + inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); look.push(...r.stats.lookaheadMs); planT.push(...r.stats.planMs); planW.push(...r.stats.planWarmMs); starve += r.stats.starveTicks || 0; cq.push(...r.stats.certQ); hits += r.stats.certHit; for (const k in r.stats.certWhy) why[k] = (why[k] || 0) + r.stats.certWhy[k]; brakes += r.stats.brakes; locks += r.stats.locks; plans += r.stats.plans; if (tLock != null) { lockTasks++; if (rc && !cf && r.t - tLock <= 3.0) recovered++; } else if (rc && !cf) recovered++; if (!rc && r.stats.locks >= 2) livelock++; if ((ci + 1) % 50 === 0) process.stderr.write(`${name} ${ci + 1}/${cases.length}\n`); } out[name] = { n: cases.length, ok, reached, collisionTasks: colTasks, collisionFrames: colFrames, uncertifiedSegs: uncert, brakes, locks, - plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, lockTasks, recovered3s: recovered, recoveryRate: cases.length ? +(recovered / cases.length).toFixed(4) : null, livelock, + plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, starveTicks: starve, starvePerPlan: plans ? +(starve / plans).toFixed(3) : null, lockTasks, recovered3s: recovered, recoveryRate: cases.length ? +(recovered / cases.length).toFixed(4) : null, livelock, timeP50: pct(times, 0.5), inferMsP50: pct(inf, 0.5), inferMsP99: pct(inf, 0.99), safetyMsP50: pct(saf, 0.5), safetyMsP99: pct(saf, 0.99), // 2026-10-18 第六轮(审查 B-1/B-2):前瞻、整次规划耗时,每次规划的距离查询数与记忆命中率,认证失败原因——与查看器 autotest 同口径对照 lookaheadMsP99: pct(look, 0.99), planMsP50: pct(planT, 0.5), planMsP99: pct(planT, 0.99), planWarmMsP99: pct(planW, 0.99), certQueriesP50: pct(cq, 0.5), certQueriesP99: pct(cq, 0.99), diff --git a/webapp/js/ik-policy.js b/webapp/js/ik-policy.js index 1b5f34d..4b3b1b1 100644 --- a/webapp/js/ik-policy.js +++ b/webapp/js/ik-policy.js @@ -191,14 +191,14 @@ class Runner { this.o = Object.assign({ K: 8, commit: 3, margin: 5, /* 2026-10-18:运行时投影余量保持 5 mm(真机护栏,不迁就数据分布);数据侧另行过滤认证间隙 < 3 mm 的老师轨迹,APG 训练余量 3 mm */ projMax: 0.5, refineMm: 5, arriveMm: 1, arriveDeg: 0.5, stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, subRule: 'cross', subLeadSec: 0.02, seed: 12345, lookahead: true, - actScale: 1, certBudget: 40, safetyMs: 30, firstMs: 45, fbGain: 0, segCert: true }, opts || {}); + actScale: 1, certBudget: 40, safetyMs: 30, firstMs: 45, fbGain: 0, segCert: true, select: 'consist' }, opts || {}); /* 2026-10-19:actScale = 模型 meta.act_scale(动作标签按 act_scale·STEP 归一化,旧模型 1);增量 = clamp(act, ±1)·actScale·STEP, 与 ml/sim/closed_loop.py 同口径。subRule:'cross'(默认,越过顶点即切,与训练标签同口径;见 _subAdvance)/ 'timeout'(旧:< subReachDeg 或 subTimeout 超时)。 */ // 每条候选最多 40 次距离查询;安全层总预算 30 ms,第 1 条候选可用到 45 ms(超出按刹停处理) this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 }); this.rng = makeRng(this.o.seed); /* 2026-10-18 第六轮(审查 B-1/B-2):耗时拆开记——inferMs 主推理、lookaheadMs 前瞻(第二次推理 + 8 份特征)、safetyMs 只含认证、 planMs 整次规划(R9 口径:必须 < 60 ms 周期);certQ 每次规划新增的距离查询数,certHit 记忆命中数,certWhy 认证失败原因计数 */ - this.stats = { plans: 0, inferMs: [], lookaheadMs: [], safetyMs: [], planMs: [], planWarmMs: [], planColdMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0, partial: 0 }; + this.stats = { plans: 0, inferMs: [], lookaheadMs: [], safetyMs: [], planMs: [], planWarmMs: [], planColdMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0, partial: 0, starveTicks: 0 }; this.reset(null); } reset(q0) { @@ -302,11 +302,14 @@ class Runner { async plan() { if (!this.needPlan()) return; this.planning = true; + const qlen0 = this.queue.length; // 规划开始时队列里可执行步数 const t = now(), c0 = this.cert.counters(), wasCold = this.cold; // 第八轮:冷启动(含前瞻)与稳态分开记,R9 判稳态 try { await this._plan(); } finally { this.planning = false; this._until = undefined; const c1 = this.cert.counters(); const dt = now() - t; this.stats.planMs.push(dt); (wasCold ? this.stats.planColdMs : this.stats.planWarmMs).push(dt); this.stats.certQ.push(c1.queries - c0.queries); this.stats.certHit += c1.hits - c0.hits; + /* 50 Hz 饥饿估计:规划耗时 dt 期间每 20 ms 消耗一步;超过 qlen0 的步数即队列空转 tick */ + this.stats.starveTicks += Math.max(0, Math.ceil(dt / (DT * 1000)) - qlen0); } } async _plan() { @@ -344,27 +347,21 @@ class Runner { if (sb[12] > 0.5) { this.subs = [0, 1].map(v => this.q.map((x, j) => x + sb[v * 6 + j] * 180)); this.subPrev = this.q.slice(); } } /* 候选:去掉前缀后的提交步,投影,打分 */ - const cands = [], near = S.criticalPairs(qe, o.margin + 10).slice(0, 64); + const cands = [], near = o.projMax > 0 ? S.criticalPairs(qe, o.margin + 10).slice(0, 64) : []; // 2026-10-20:全程目标反馈(与 ml/sim/closed_loop.py 的 fb_gain 一致)——一次 DLS,加到所有候选所有步 const fbStep = (o.fbGain > 0 && this.goal) ? refineStep(S, qe, this.goal.T, this.goal.R, 0.3).dq : null; for (let k = 0; k < K; k++) { const inc = this._cand(out.act, k, r); if (fbStep) for (let s = 0; s < H; s++) for (let j = 0; j < 6; j++) inc[s][j] += o.fbGain * fbStep[j]; - let steps = inc.slice(r, r + o.commit), q = qe.slice(); - const adj = []; - for (const d of steps) { - let qn = q.map((v, j) => v + d[j]); - let qp = project(S, qn, o.margin, o.projMax, near); - /* 限位轴先夹到 LIM(与执行 clamp_box 同口径),否则候选会被认证的 inBox 整段拒绝 → 永远刹停 */ - qp = qp.map((v, j) => { const L = S.LIM[S.ORDER[j]]; return L.cont ? v : Math.max(L.lo, Math.min(L.hi, v)); }); - if (qp !== qn) this.stats.projected++; - adj.push(qp.map((v, j) => v - q[j])); q = qp; - } - steps = adj; + const steps = inc.slice(r, r + o.commit); // 先不投影;只对进入认证的候选投影(2026-10-20:投影 8 份候选占安全层 ~13 ms) const fut = []; let qq = qe.slice(); for (let s = r; s < H; s++) { qq = qq.map((v, j) => v + inc[s][j]); fut.push(qq); } let j1 = 0; for (let s = 0; s < H; s++) j1 += inc[s][0]; let score = -2.0 * out.value[k]; - if (this.lastPlan) { // 与上一条已选计划的未来重叠步对齐:lastPlan[i] = 旧 qe 走 i 步,新 qe = lastPlan[commit],fut[s] 对应 lastPlan[s+commit+1] + if (o.select === 'endpoint') { + /* 2026-10-20:端点进度选择(与 ml/sim/closed_loop.py select='endpoint' 同口径)——n=512 时间效率 29→35%、ratio2s 0.343→0.285 */ + const qa = fut[fut.length - 1], pe = S.fk(qa, 0); + score = -Math.hypot(pe[0] - this.goal.T[0], pe[1] - this.goal.T[1], pe[2] - this.goal.T[2]); + } else if (this.lastPlan) { // 与上一条已选计划的未来重叠步对齐:lastPlan[i] = 旧 qe 走 i 步,新 qe = lastPlan[commit],fut[s] 对应 lastPlan[s+commit+1] // 2026-10-19 修错位(原对齐 lastPlan[s+commit],奖励比上一计划慢一步的候选;Python closed_loop.py 同步修正) let c = 0, w = 1; for (let s = 0; s < fut.length && s + o.commit + 1 < this.lastPlan.length; s++, w *= 0.85) { @@ -387,7 +384,7 @@ class Runner { /* 短前瞻:冷启动(新目标 / 目标跳变)时,各候选块末态再推一块,用其价值打分。 2026-10-18 第六轮:去掉"卡死时也前瞻"——与仿真 ml/sim/closed_loop.py(只在首块前瞻)对齐; 前瞻要算 8 份特征(8 次 gapMatrix cap40,沙箱 Node 实测 P99 126 ms),卡死时再做会让整次规划连续超周期。卡死由换向锁处理。 */ - if (o.lookahead && this.cold) { + if (o.lookahead && this.cold && o.select !== 'endpoint') { this.stats.lookahead++; const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6); pool.forEach((c, i) => { @@ -410,7 +407,19 @@ class Runner { for (const c of pool) { if (c !== pool[0] && now() - t1 > o.safetyMs) break; this._until = t1 + (c === pool[0] ? Math.max(o.firstMs, o.safetyMs) : o.safetyMs); - const v = this._certCommit(qe, c.steps, true); + let steps = c.steps; + if (o.projMax > 0) { // 投影只做给要认证的候选(按需),不再对全部 K 条预算 + const adj = []; let q = qe.slice(); + for (const d of c.steps) { + const qn = q.map((v, j) => v + d[j]); + let qp = project(S, qn, o.margin, o.projMax, near); + qp = qp.map((v, j) => { const L = S.LIM[S.ORDER[j]]; return L.cont ? v : Math.max(L.lo, Math.min(L.hi, v)); }); + if (qp !== qn) this.stats.projected++; + adj.push(qp.map((v, j) => v - q[j])); q = qp; + } + steps = adj; + } + const v = this._certCommit(qe, steps, true); if (v.ok) { picked = c; pickedSteps = v.steps; this.brakeTail = v.tail; break; } } this._until = undefined;