Files

384 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
* ik-policy.js —— Transformer 避障策略的闭环运行时(浏览器 / Node 通用,UMD)
* 方案文档 §5 / §8:50 Hz 执行,每 3 步(60 ms)推理一次,8 个噪声 → 8 条 16 步动作块,
* 1 步胶囊梯度投影 → 排序 → 距离下界认证(前 3 步 + 刹停段)→ 提交;全不过则沿已认证刹停段停。
* 不依赖任何规划器(solveDyn / routeRT / 路网):只用求解器的 fk / orient / gapMatrix / 胶囊对间隙。
* 特征定义与 ml/common/features.py 逐项一致(ml/eval 对拍)。改特征必须同时改两边并升 FEAT_VER。
* ==========================================================================*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory();
else root.ArmPolicy = factory();
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const FEAT_VER = 1;
const VMAX = [60, 60, 90, 90, 120, 180]; // °/s(与 ml/gen/timelaw.cjs 同)
const DT = 0.02;
const STEP = VMAX.map(v => v * DT); // 每步最大增量(°)
const ACC = 5.0; // 归一化加速度 1/s²(与时间律同)
const H = 16, NJ = 6, PREFIX = 3;
const GP = [[0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [2, 4], [2, 5], [3, 5]]; // 特征里的 8 个组对
const GAP_CAP = 40;
const F_DIM = 83;
const D2R = Math.PI / 180;
const wrap180 = a => { a = ((a + 180) % 360 + 360) % 360 - 180; return a; };
/* ---------------- 特征(83 维) ---------------- */
function features(S, st, gm) {
const q = st.q, L = S.LIM, O = S.ORDER, f = new Float32Array(F_DIM);
let o = 0;
for (let j = 0; j < 6; j++) { f[o++] = Math.sin(q[j] * D2R); f[o++] = Math.cos(q[j] * D2R); }
for (const j of [1, 3, 4]) { const t = L[O[j]]; f[o++] = 2 * (q[j] - t.lo) / (t.hi - t.lo) - 1; }
const qp = st.qPrev || q;
for (let j = 0; j < 6; j++) { const d = L[O[j]].cont ? wrap180(q[j] - qp[j]) : q[j] - qp[j]; f[o++] = d / STEP[j]; }
const p = S.fk(q, 0), R = S.orient(q);
for (let k = 0; k < 3; k++) f[o++] = p[k] / 600;
for (const k of [0, 3, 6, 1, 4, 7]) f[o++] = R[k];
for (let k = 0; k < 3; k++) f[o++] = st.T[k] / 600;
if (st.R) for (const k of [0, 3, 6, 1, 4, 7]) f[o++] = st.R[k]; else o += 6;
for (let k = 0; k < 3; k++) f[o++] = (st.T[k] - p[k]) / 600;
for (let k = 0; k < 3; k++) f[o++] = Math.max(-1, Math.min(1, (st.T[k] - p[k]) / 50));
f[o++] = st.R ? 0 : 1; f[o++] = st.R ? 1 : 0;
const pre = st.prefix || [];
for (let k = 0; k < PREFIX; k++) for (let j = 0; j < 6; j++) f[o++] = k < pre.length ? pre[k][j] / STEP[j] : 0;
for (let k = 0; k < PREFIX; k++) f[o++] = k < pre.length ? 1 : 0;
if (st.sub) { for (let j = 0; j < 6; j++) { const d = L[O[j]].cont ? wrap180(st.sub[j] - q[j]) : st.sub[j] - q[j]; f[o++] = d / 180; } f[o++] = 1; }
else o += 7;
const G = gm || S.gapMatrix(q, GAP_CAP);
for (const [a, b] of GP) f[o++] = Math.min(G[a * 7 + b], GAP_CAP) / GAP_CAP;
if (o !== F_DIM) throw new Error('feature dim ' + o);
return f;
}
/* ---------------- 距离下界认证(ml/gen/cert.cjs 复用本实现) ---------------- */
function makeCert(S, opt) {
opt = opt || {};
const CAP = opt.cap || 40, MIN_DEG = opt.minDeg || 0.002;
/* 分组杠杆表 TL[k][g]:组对 (gA,gB) 只用关节 gA..gB−1 带动组 gB 的杠杆(比全局上界紧得多) */
const TL = opt.leverTable || S.leverTable(4000);
const L = S.LIM, O = S.ORDER;
const memo = new Map();
const key = q => q.map(v => v.toFixed(5)).join(',');
let queries = 0, hits = 0, budgetLeft = Infinity, deadline = Infinity;
const clock = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
const gm = q => { const k = key(q); let M = memo.get(k); if (!M) { M = S.gapMatrix(q, CAP); queries++; budgetLeft--; if (memo.size > 50000) memo.clear(); memo.set(k, M); } else hits++; return M; };
const minGap = M => { let m = Infinity; for (let a = 0; a < 7; a++) for (let b = a + 2; b < 7; b++) m = Math.min(m, M[a * 7 + b]); return m; };
const inBox = q => O.every((id, i) => L[id].cont || (q[i] >= L[id].lo - 1e-6 && q[i] <= L[id].hi + 1e-6));
function seg(a, b, depth) {
if (budgetLeft <= 0) return false;
if (deadline !== Infinity && clock() > deadline) { budgetLeft = 0; return false; } // 超时与超预算同样判不过(→ 刹停,保守)
const Ma = gm(a), Mb = gm(b);
const dq = a.map((v, i) => Math.abs(b[i] - v));
let ok = true;
for (let ga = 0; ga < 7 && ok; ga++) for (let gb = ga + 2; gb < 7; gb++) {
const lhs = Ma[ga * 7 + gb] + Mb[ga * 7 + gb];
let rhs = 0; for (let k = ga; k < gb; k++) rhs += TL[k][gb] * dq[k];
if (!(lhs > rhs)) { ok = false; break; }
}
if (ok) return true;
if (minGap(Ma) <= 0 || minGap(Mb) <= 0) return false;
if (Math.max.apply(null, dq) < MIN_DEG || depth > 40) return false;
const m = a.map((v, i) => 0.5 * (v + b[i]));
return seg(a, m, depth + 1) && seg(m, b, depth + 1);
}
/* 折线(相邻点按短弧展开)→ {ok, queries, minGap, failAt, why};budget = 本次最多新增距离查询数(超出判不过,why='budget')
2026-10-18 第六轮(审查 B-2):可选 until = 墙钟截止时刻(performance.now 口径),到点即判不过。
此前 Runner 只在两条候选之间看 30 ms 预算,单条候选内部 40 次查询 × 8 条可把安全层拖到 250 ms。 */
function path(pts, budget, until) {
const q0 = queries; let mg = Infinity;
budgetLeft = budget || Infinity; deadline = until || Infinity;
const P = [pts[0].slice()];
for (let i = 1; i < pts.length; i++) P.push(P[i - 1].map((v, k) => L[O[k]].cont ? v + wrap180(pts[i][k] - v) : pts[i][k]));
for (let i = 0; i < P.length; i++) { if (!inBox(P[i])) return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why: 'box' }; }
for (let i = 0; i < P.length; i++) mg = Math.min(mg, minGap(gm(P[i])));
for (let i = 1; i < P.length; i++) if (!seg(P[i - 1], P[i], 0)) { const why = deadline !== Infinity && clock() > deadline ? 'time' : budgetLeft <= 0 ? 'budget' : 'cert'; budgetLeft = Infinity; deadline = Infinity; return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why }; }
budgetLeft = Infinity; deadline = Infinity;
return { ok: true, queries: queries - q0, minGap: mg, unwrapped: P };
}
return { seg: (a, b) => { budgetLeft = Infinity; deadline = Infinity; return seg(a, b, 0); }, path, leverTable: TL, gapMin: q => minGap(gm(q)), gm, clear: () => memo.clear(),
counters: () => ({ queries, hits }) };
}
/* ---------------- 刹停段:从 q 以每步增量 v 起,按 ACC 均匀减速到 0 ---------------- */
function brakeSteps(v) {
let n = 0; for (let j = 0; j < 6; j++) n = Math.max(n, Math.ceil(Math.abs(v[j]) / (STEP[j] * ACC * DT) - 1e-9));
const out = [];
for (let k = 1; k <= n; k++) out.push(v.map(x => x * (1 - k / (n + 1))));
return out;
}
/* ---------------- 1 步胶囊梯度投影(每轴 ≤ maxDeg,仅 gap < margin) ---------------- */
/* pairs:本次推理开始时在 qe 处取的近距胶囊对(S.criticalPairs(qe, margin+10)),候选各步只对这些对求间隙与有限差分(微秒级) */
function project(S, q, margin, maxDeg, pairs) {
if (!pairs || !pairs.length) return q;
const gs = pairs.map(p => S.pairGap(q, p[0], p[1]));
const g0 = Math.min.apply(null, gs);
if (g0 >= margin) return q;
pairs = pairs.filter((p, i) => gs[i] < margin + 2).slice(0, 6);
const h = 0.01, grad = [0, 0, 0, 0, 0, 0];
for (let j = 0; j < 6; j++) {
const qa = q.slice(); qa[j] += h;
const g1 = Math.min.apply(null, pairs.map(p => S.pairGap(qa, p[0], p[1])));
grad[j] = (g1 - g0) / h;
}
const n2 = grad.reduce((a, x) => a + x * x, 0);
if (n2 < 1e-12) return q;
const need = margin - g0;
return q.map((v, j) => v + Math.max(-maxDeg, Math.min(maxDeg, need * grad[j] / n2)));
}
/* ---------------- 局部 DLS 精修(接近目标时) ---------------- */
function refineStep(S, q, T, Rt, maxScale) {
const e = x => {
const p = S.fk(x, 0), r = [T[0] - p[0], T[1] - p[1], T[2] - p[2]];
if (!Rt) return r;
const R = S.orient(x); // 旋转误差向量 ≈ 0.5·vee(R_t Rᵀ − R Rtᵀ),按 100 mm/rad 折算到 mm 量级
const M = [0, 0, 0, 0, 0, 0, 0, 0, 0];
for (let i = 0; i < 3; i++) for (let k = 0; k < 3; k++) { let s = 0; for (let m = 0; m < 3; m++) s += Rt[i * 3 + m] * R[k * 3 + m]; M[i * 3 + k] = s; }
return r.concat([0.5 * (M[7] - M[5]) * 100, 0.5 * (M[2] - M[6]) * 100, 0.5 * (M[3] - M[1]) * 100]);
};
const e0 = e(q), m = e0.length, h = 1e-3, J = [];
for (let j = 0; j < 6; j++) { const qa = q.slice(); qa[j] += h; const e1 = e(qa); J.push(e1.map((v, i) => -(v - e0[i]) / h)); }
// Δq = Jᵀ (J Jᵀ + λ²I)⁻¹ e,m ≤ 6
const lam = 0.5, A = [];
for (let i = 0; i < m; i++) { A.push([]); for (let k = 0; k < m; k++) { let s = i === k ? lam * lam : 0; for (let j = 0; j < 6; j++) s += J[j][i] * J[j][k]; A[i].push(s); } }
const y = solveLin(A, e0.slice());
const dq = [0, 0, 0, 0, 0, 0];
for (let j = 0; j < 6; j++) { let s = 0; for (let i = 0; i < m; i++) s += J[j][i] * y[i]; dq[j] = s; }
let sc = 1; for (let j = 0; j < 6; j++) sc = Math.max(sc, Math.abs(dq[j]) / (STEP[j] * (maxScale || 0.5)));
return { dq: dq.map(v => v / sc), err: Math.hypot(e0[0], e0[1], e0[2]) };
}
function solveLin(A, b) {
const n = b.length;
for (let c = 0; c < n; c++) {
let p = c; for (let r = c + 1; r < n; r++) if (Math.abs(A[r][c]) > Math.abs(A[p][c])) p = r;
[A[c], A[p]] = [A[p], A[c]]; [b[c], b[p]] = [b[p], b[c]];
for (let r = c + 1; r < n; r++) { const f = A[r][c] / A[c][c]; for (let k = c; k < n; k++) A[r][k] -= f * A[c][k]; b[r] -= f * b[c]; }
}
const x = new Array(n).fill(0);
for (let r = n - 1; r >= 0; r--) { let s = b[r]; for (let k = r + 1; k < n; k++) s -= A[r][k] * x[k]; x[r] = s / A[r][r]; }
return x;
}
/* ---------------- 正态噪声 ---------------- */
function makeRng(seed) {
let s = (seed >>> 0) || 1;
const u = () => { s = (s * 1664525 + 1013904223) >>> 0; return (s + 0.5) / 4294967296; };
return { u, n: () => Math.sqrt(-2 * Math.log(u())) * Math.cos(2 * Math.PI * u()) };
}
/* ============================================================================
* 闭环运行器
* infer(feat Float32Array[B*F], z Float32Array[B*H*6], B) → Promise<{act[B*H*6], value[B], sub[B*13]}>
* 用法:r = new Runner(S, infer, opts); r.reset(q0); r.setGoal(T, R);
* 每 20 ms 调 r.tick();r.needPlan() 为真时 await r.plan()(tick 与 plan 可并行:plan 只在队列 ≥3 步时发起)
* ==========================================================================*/
class Runner {
constructor(S, infer, opts) {
this.S = S; this.infer = infer;
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, seed: 12345, lookahead: true,
certBudget: 40, safetyMs: 30, firstMs: 45 }, opts || {}); // 每条候选最多 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 };
this.reset(null);
}
reset(q0) {
this.q = q0 ? q0.slice() : null; this.qPrev = this.q ? this.q.slice() : null;
this.queue = []; this.brakeTail = []; this.lastPlan = null; this.goal = null;
this.subs = []; this.subT = 0; this.vals = []; this.brakeRun = 0; this.lock = null; this.t = 0; this.cold = true; this.done = false;
}
setGoal(T, R) {
const jump = !this.goal || Math.hypot(T[0] - this.goal.T[0], T[1] - this.goal.T[1], T[2] - this.goal.T[2]) > 50 || (!!R !== !!this.goal.R);
this.goal = { T: T.slice(), R: R ? R.slice() : null };
if (jump) { this.needSub = true; this.subs = []; this.cold = true; this.vals = []; this.lock = null; }
this.done = false;
}
qEnd() { let q = this.q.slice(); for (const d of this.queue) q = q.map((v, j) => v + d[j]); return q; }
err(q) {
const p = this.S.fk(q, 0), T = this.goal.T;
const e = Math.hypot(T[0] - p[0], T[1] - p[1], T[2] - p[2]);
let oe = 0;
if (this.goal.R) { const R = this.S.orient(q); let tr = 0; for (let i = 0; i < 9; i++) tr += R[i] * this.goal.R[i]; oe = Math.acos(Math.max(-1, Math.min(1, (tr - 1) / 2))) / D2R; }
return { e, oe };
}
needPlan() { return !!this.goal && !this.done && !this.planning && this.queue.length <= this.o.commit; }
tick() {
if (!this.q) return null;
const d = this.queue.shift();
this.qPrev = this.q.slice();
if (d) this.q = this.q.map((v, j) => v + d[j]);
this.t += DT;
if (this.goal && !this.queue.length) {
const { e, oe } = this.err(this.q);
if (e < this.o.arriveMm && oe < this.o.arriveDeg) this.done = true;
}
return this.q;
}
_cand(act, k, r) { // 第 k 条候选:增量(°),丢掉模型的前 r 步(已提交前缀),取接下来的步
const inc = [];
for (let s = 0; s < H; s++) { const row = []; for (let j = 0; j < 6; j++) row.push(Math.max(-1, Math.min(1, act[(k * H + s) * 6 + j])) * STEP[j]); inc.push(row); }
return inc;
}
_certCommit(qe, steps) { // 认证:提交步 + 刹停段(先用离散判定快速剔除,再做距离下界认证)
const pts = [qe.slice()]; let q = qe.slice();
for (const d of steps) { q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
for (let i = 1; i < pts.length; i++) if (!this.S.collideFree(pts[i])) return { ok: false, tail: [] };
const tail = brakeSteps(steps.length ? steps[steps.length - 1] : [0, 0, 0, 0, 0, 0]);
/* 刹停段各步同向等比减速 → 关节空间里是一条直线,认证终点一段即可 */
if (tail.length) { for (const d of tail) q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
const c = this.cert.path(pts, this.o.certBudget, this._until);
if (!c.ok) this.stats.certWhy[c.why || 'collide'] = (this.stats.certWhy[c.why || 'collide'] || 0) + 1;
return { ok: c.ok, tail };
}
async plan() {
if (!this.needPlan()) return;
this.planning = true;
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;
}
}
async _plan() {
const S = this.S, o = this.o, K = o.K;
const r = Math.min(this.queue.length, PREFIX), prefix = this.queue.slice(0, r);
const qe = this.qEnd();
this.stats.plans++;
/* 接近目标:局部 DLS 精修代替模型 */
const ee = this.err(qe);
if (ee.e < o.refineMm && (!this.goal.R || ee.oe < 5)) {
if (ee.e < o.arriveMm && ee.oe < o.arriveDeg) return;
const steps = []; let q = qe.slice();
for (let k = 0; k < o.commit; k++) { const rs = refineStep(S, q, this.goal.T, this.goal.R, 0.3); steps.push(rs.dq); q = q.map((v, j) => v + rs.dq[j]); }
const c = this._certCommit(qe, steps);
if (c.ok) { this.queue.push(...steps); this.brakeTail = c.tail; this.stats.refine++; return; }
}
/* 子目标推进 */
if (this.subs.length) {
const sg = this.subs[0]; let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs((S.LIM[S.ORDER[j]].cont ? wrap180(sg[j] - qe[j]) : sg[j] - qe[j])));
if (m < o.subReachDeg || this.t - this.subT > o.subTimeout) { this.subs.shift(); this.subT = this.t; }
}
const gm = S.gapMatrix(this.q, GAP_CAP);
const st = { q: this.q, qPrev: this.qPrev, T: this.goal.T, R: this.goal.R, prefix, sub: this.subs[0] || null };
const f = features(S, st, gm);
const feat = new Float32Array(K * F_DIM), z = new Float32Array(K * H * 6);
for (let k = 0; k < K; k++) feat.set(f, k * F_DIM);
for (let i = 0; i < z.length; i++) z[i] = this.rng.n();
const t0 = now();
const out = await this.infer(feat, z, K, true); // true:K 行特征相同,enc_once 模型只送 1 行
this.stats.inferMs.push(now() - t0);
let t1 = now();
/* 子目标:新目标/跳变后取价值最好的那条输出 */
let best = 0; for (let k = 1; k < K; k++) if (out.value[k] < out.value[best]) best = k;
if (this.needSub) {
this.needSub = false; this.subT = this.t;
const sb = out.sub.subarray(best * 13, best * 13 + 13);
if (sb[12] > 0.5) this.subs = [0, 1].map(v => this.q.map((x, j) => x + sb[v * 6 + j] * 180));
}
/* 候选:去掉前缀后的提交步,投影,打分 */
const cands = [], near = S.criticalPairs(qe, o.margin + 10).slice(0, 64);
for (let k = 0; k < K; k++) {
const inc = this._cand(out.act, k, r);
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]);
const qp = project(S, qn, o.margin, o.projMax, near);
if (qp !== qn) this.stats.projected++;
adj.push(qp.map((v, j) => v - q[j])); q = qp;
}
steps = adj;
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) { // 与上一条已选计划的未来重叠步对齐(时间平移 3 步)
let c = 0, w = 1;
for (let s = 0; s < fut.length && s + o.commit < this.lastPlan.length; s++, w *= 0.85) {
let d2 = 0; for (let j = 0; j < 6; j++) d2 += Math.pow((fut[s][j] - this.lastPlan[s + o.commit][j]) / STEP[j], 2);
c += w * d2;
}
score -= 0.02 * c;
}
cands.push({ k, steps, fut, j1, score, value: out.value[k] });
}
/* 卡死检测:价值 0.3 s 内不降 或 连续 2 次刹停 → 锁定另一绕行方向 1 s */
const vbest = out.value[best]; this.vals.push(vbest); if (this.vals.length > o.stuckPlans) this.vals.shift();
const stuck = this.brakeRun >= 2 || (this.vals.length >= o.stuckPlans && this.vals[this.vals.length - 1] > this.vals[0] * 0.99 && ee.e > 20);
if (stuck && !(this.lock && this.t < this.lock.until)) {
const lastDir = this.lastPlan ? Math.sign(this.lastPlan[this.lastPlan.length - 1][0] - this.lastPlan[0][0]) || 1 : 1;
this.lock = { side: -lastDir, until: this.t + o.lockSec }; this.stats.locks++; this.vals = [];
}
let pool = cands;
if (this.lock && this.t < this.lock.until) { const f2 = cands.filter(c => Math.abs(c.j1) < 2 || Math.sign(c.j1) === this.lock.side); if (f2.length) pool = f2; }
/* 短前瞻:冷启动(新目标 / 目标跳变)时,各候选块末态再推一块,用其价值打分。
2026-10-18 第六轮:去掉"卡死时也前瞻"——与仿真 ml/sim/closed_loop.py(只在首块前瞻)对齐;
前瞻要算 8 份特征(8 次 gapMatrix cap40,沙箱 Node 实测 P99 126 ms),卡死时再做会让整次规划连续超周期。卡死由换向锁处理。 */
if (o.lookahead && this.cold) {
this.stats.lookahead++;
const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6);
pool.forEach((c, i) => {
const qa = c.fut[c.fut.length - 1], qb = c.fut[Math.max(0, c.fut.length - 2)];
feat2.set(features(S, { q: qa, qPrev: qb, T: this.goal.T, R: this.goal.R, prefix: [], sub: null }), i * F_DIM);
});
for (let i = 0; i < z2.length; i++) z2[i] = this.rng.n();
const o2 = await this.infer(feat2, z2, pool.length, false); // 各行特征不同:必须逐行编码(第六轮修:此前 enc_once 下只送了第 1 行,前瞻价值全错)
pool.forEach((c, i) => { c.score = -2.0 * o2.value[i] - 0.5 * c.value; });
this.stats.lookaheadMs.push(now() - t1); t1 = now();
}
this.cold = false;
pool.sort((a, b) => b.score - a.score);
/* 安全层:依次认证,第一个通过即提交 */
/* 预算(第六轮,审查 B-2):浏览器实测安全层 P99 249 ms。拆开量(沙箱 Node,每次调用):gapMatrix cap20 3.6 ms / cap40 5.5 ms、
collideFree 1.5 ms、criticalPairs 3.1 ms,推理本身 1.5 ms——耗时在几何查询,不在模型。此前截止只在候选之间检查,
单条候选最多 40 次查询(≈150 ms)不受限。现在墙钟截止下沉到认证内部:第 1 条候选截止 t1+firstMs,其余共享到 t1+safetyMs;
到点判不过 → 沿上次已认证的刹停段停,安全性不变,只是可能多刹停。 */
let picked = null;
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); if (v.ok) { picked = c; this.brakeTail = v.tail; break; }
}
this._until = undefined;
this.stats.safetyMs.push(now() - t1);
if (picked) {
this.queue.push(...picked.steps); this.lastPlan = [qe.slice()].concat(picked.fut); this.brakeRun = 0;
} else {
this.stats.brakes++; this.brakeRun++;
if (this.brakeTail.length) { this.queue.push(...this.brakeTail); this.brakeTail = []; }
this.lastPlan = null;
}
}
}
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
/* ---------------- onnxruntime 适配(web: ort = window.ort;node: require('onnxruntime-node')) ---------------- */
/* 2026-10-18 第五轮:模型 meta.enc_once=true(编码一次、解码 K 次)时只送第 1 行特征 [1,F];
Runner 仍按 K 行组织特征(K 行本就相同),旧模型(enc_once 缺省)照旧送 [B,F]。 */
/* 2026-10-18 第六轮:第 4 个参数 shared 表示 K 行特征相同(主推理)。enc_once 模型的 feat 轴是动态的(1 或 K 行):
shared → 送 1 行(编码一次);前瞻各行不同 → 送 B 行逐行编码。 */
function ortInfer(ort, session, opt) {
const encOnce = !!(opt && opt.encOnce), featDyn = !!(opt && opt.featDyn); // featDyn:meta.feat_dyn(第六轮导出),feat 轴可为 1 或 K
const run = async (ft, zz, B) => { const r = await session.run({ feat: ft, z: new ort.Tensor('float32', zz, [B, H, 6]) }); return { act: r.act.data, value: r.value.data, sub: r.sub.data }; };
return async (feat, z, B, shared) => {
if (!encOnce) return run(new ort.Tensor('float32', feat.slice(0, B * F_DIM), [B, F_DIM]), z, B);
if (shared !== false) return run(new ort.Tensor('float32', feat.slice(0, F_DIM), [1, F_DIM]), z, B);
if (featDyn) return run(new ort.Tensor('float32', feat.slice(0, B * F_DIM), [B, F_DIM]), z, B);
/* 第五轮导出的 enc_once 模型 feat 轴固定为 1:前瞻逐行各推一次(正确但慢,重新导出即可) */
const act = new Float32Array(B * H * 6), value = new Float32Array(B), sub = new Float32Array(B * 13);
for (let i = 0; i < B; i++) {
const r = await run(new ort.Tensor('float32', feat.slice(i * F_DIM, (i + 1) * F_DIM), [1, F_DIM]), z.slice(i * H * 6, (i + 1) * H * 6), 1);
act.set(r.act, i * H * 6); value[i] = r.value[0]; sub.set(r.sub, i * 13);
}
return { act, value, sub };
};
}
return { FEAT_VER, VMAX, DT, STEP, ACC, H, F_DIM, GP, GAP_CAP, features, makeCert, brakeSteps, project, refineStep, Runner, ortInfer, wrap180 };
}));