Files
Robot_Arm/webapp/js/ik-policy.js
T

352 lines
20 KiB
JavaScript
Raw 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, budgetLeft = Infinity;
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); } 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;
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') */
function path(pts, budget) {
const q0 = queries; let mg = Infinity;
budgetLeft = budget || 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)) return { ok: false, queries: queries - q0, minGap: mg, failAt: i, why: budgetLeft <= 0 ? 'budget' : 'cert' };
budgetLeft = Infinity;
return { ok: true, queries: queries - q0, minGap: mg, unwrapped: P };
}
return { seg: (a, b) => { budgetLeft = Infinity; return seg(a, b, 0); }, path, leverTable: TL, gapMin: q => minGap(gm(q)), gm, clear: () => memo.clear() };
}
/* ---------------- 刹停段:从 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 }, opts || {}); // 每条候选最多 40 次距离查询;每次推理安全层总预算 30 ms(超出按刹停处理)
this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 });
this.rng = makeRng(this.o.seed);
this.stats = { plans: 0, inferMs: [], safetyMs: [], 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);
return { ok: c.ok, tail };
}
async plan() {
if (!this.needPlan()) return;
this.planning = true;
try { await this._plan(); } finally { this.planning = false; }
}
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);
this.stats.inferMs.push(now() - t0);
const 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; }
/* 短前瞻:冷启动 / 卡死时,各候选块末态再推一块,用其价值打分 */
if (o.lookahead && (this.cold || stuck)) {
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);
pool.forEach((c, i) => { c.score = -2.0 * o2.value[i] - 0.5 * c.value; });
}
this.cold = false;
pool.sort((a, b) => b.score - a.score);
/* 安全层:依次认证,第一个通过即提交 */
let picked = null;
for (const c of pool) {
if (now() - t1 > o.safetyMs && picked === null && c !== pool[0]) break;
const v = this._certCommit(qe, c.steps); if (v.ok) { picked = c; this.brakeTail = v.tail; break; }
}
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]。 */
function ortInfer(ort, session, opt) {
const encOnce = !!(opt && opt.encOnce);
return async (feat, z, B) => {
const ft = encOnce ? new ort.Tensor('float32', feat.subarray(0, F_DIM), [1, F_DIM]) : new ort.Tensor('float32', feat, [B, F_DIM]);
const r = await session.run({ feat: ft, z: new ort.Tensor('float32', z, [B, H, 6]) });
return { act: r.act.data, value: r.value.data, sub: r.sub.data };
};
}
return { FEAT_VER, VMAX, DT, STEP, ACC, H, F_DIM, GP, GAP_CAP, features, makeCert, brakeSteps, project, refineStep, Runner, ortInfer, wrap180 };
}));