diff --git a/ml/gen/README.md b/ml/gen/README.md new file mode 100644 index 0000000..f3359cc --- /dev/null +++ b/ml/gen/README.md @@ -0,0 +1,27 @@ +# ml/gen — M1 离线数据生成(在高性能机器上跑) + +依赖:Node ≥ 18,仓库根目录执行。输出写到 `ml/data/`(已 gitignore)。 + +## 1. 老师一致性门禁(先跑,决定是否需要共识过滤) +``` +node ml/gen/consistency.cjs 300 4242 > ml/data/consistency.json +``` +单核约 25–30 分钟(300 任务 × 6 次老师求解)。看 `consistentByGateRule`(pos 任务忽略 J6): +- ≥ 0.90 → 生成时不加 `--consensus` +- < 0.90 → 生成时加 `--consensus 4`(过滤与邻居绕法不一致的样本) + +## 2. 生成数据 +``` +bash ml/gen/run_parallel.sh <每进程条数> <进程数> [起始种子] [额外参数] +# 例:16 核、5 万条、不共识: +bash ml/gen/run_parallel.sh 3125 16 +# 例:16 核、5 万条、共识 4 邻居: +bash ml/gen/run_parallel.sh 3125 16 1 "--consensus 4" +``` +耗时估算(沙箱实测每条每核约 2.1 s;高性能 CPU 单核通常快 1.5–2 倍): +- 不共识:约 30 核·时 → 16 核约 1–2 小时 +- 共识 4:约 150 核·时 → 16 核约 5–9 小时 + +## 3. 回传给我 +- `ml/data/stats.json`、`ml/data/consistency.json`(必须) +- `ml/data/shard_*.jsonl`(训练数据,体积约 2.8 MB/500 条 → 5 万条约 280 MB,可压缩后放网盘或直接留在本机用于 M3/M5) diff --git a/ml/gen/consistency.cjs b/ml/gen/consistency.cjs index 58c991b..80384c9 100644 --- a/ml/gen/consistency.cjs +++ b/ml/gen/consistency.cjs @@ -8,6 +8,8 @@ const N = +process.argv[2] || 100, SEED = +process.argv[3] || 4242; const { S, ArmIK } = require('../../tests/route_bench_setup.cjs')(); const sm = require('./sampler.cjs')(S, SEED); const teach = require('./teacher.cjs')(S, ArmIK); +const PD = require('./pathdev.cjs'); +const fz = (r, t) => (t.mode === 'pos' && r.ptsJ6Frozen) ? r.ptsJ6Frozen : r.pts; // 与 gen 同口径:pos 任务冻结 J6(gen 里认证不过才回退) const { VMAX } = require('./timelaw.cjs'); function resample(P, n) { const seg = []; for (let i = 1; i < P.length; i++) { let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(P[i][j] - P[i - 1][j]) / VMAX[j]); seg.push(m); } @@ -22,16 +24,16 @@ const axisHist = [0, 0, 0, 0, 0, 0]; let sameNoJ6 = 0; let tasks = 0, pairs = 0, same = 0, detSame = 0, detN = 0; const devs = [], byCat = {}; while (tasks < N) { const t = sm.sample(); if (t.cat !== 'B' && t.cat !== 'C') continue; - const base = teach(t); if (!base.ok || !base.reached) continue; + const base = teach(t); if (!base.ok || !base.reached) continue; base.pts = fz(base, t); tasks++; - const again = teach(t); if (again.ok) { detN++; if (dev(base.pts, again.pts) < 1e-6) detSame++; } + const again = teach(t); if (again.ok) { detN++; if (dev(base.pts, fz(again, t)) < 1e-6) detSame++; } for (let k = 0; k < 4; k++) { const u = [sm.rnd() - 0.5, sm.rnd() - 0.5, sm.rnd() - 0.5], n = Math.hypot(...u); const T2 = t.T.map((v, i) => v + 5 * u[i] / n); const r = teach(Object.assign({}, t, { T: T2 })); if (!r.ok || !r.reached) continue; - const da = devAx(base.pts, r.pts), d = Math.max(...da); devs.push(d); pairs++; - if (Math.max(...da.slice(0, 5)) < 30) sameNoJ6++; + const da = devAx(base.pts, fz(r, t)), d = Math.max(...da); devs.push(d); pairs++; + if (PD.sameClass(base.pts, fz(r, t), t.mode)) sameNoJ6++; if (d >= 30) axisHist[da.indexOf(d)]++; const c = byCat[t.cat] = byCat[t.cat] || { pairs: 0, same: 0 }; c.pairs++; if (d < 30) { same++; c.same++; } @@ -42,4 +44,4 @@ devs.sort((a, b) => a - b); console.log(JSON.stringify({ tasks, pairs, consistentRate: +(same / pairs).toFixed(3), gate: same / pairs >= 0.9 ? 'PASS' : 'FAIL', devP50: +devs[devs.length >> 1].toFixed(1), devP90: +devs[Math.floor(devs.length * 0.9)].toFixed(1), byCat: Object.fromEntries(Object.entries(byCat).map(([k, v]) => [k, +(v.same / v.pairs).toFixed(3)])), - consistentIgnoringJ6: +(sameNoJ6 / pairs).toFixed(3), worstAxisOfInconsistent: axisHist, deterministic: detSame + '/' + detN })); + consistentByGateRule: +(sameNoJ6 / pairs).toFixed(3), gateByRule: sameNoJ6 / pairs >= 0.9 ? 'PASS' : 'FAIL', worstAxisOfInconsistent: axisHist, deterministic: detSame + '/' + detN })); diff --git a/ml/gen/gen_dataset.cjs b/ml/gen/gen_dataset.cjs index e29c525..e1407bc 100644 --- a/ml/gen/gen_dataset.cjs +++ b/ml/gen/gen_dataset.cjs @@ -1,5 +1,7 @@ /* M1 数据生成器(2026-10-17,方案 §4) - * node ml/gen/gen_dataset.cjs --n 500 --seed 1 --out ml/data/shard_0001.jsonl + * node ml/gen/gen_dataset.cjs --n 500 --seed 1 --out ml/data/shard_0001.jsonl [--consensus 4] + * --consensus k:邻域共识过滤。起点不变、目标位置各扰动 5 mm 造 k 个邻居跑老师,原任务路径若与多数邻居 + * 不在同一绕行类(pathdev.sameClass),整条样本 keep=false(why=consensus)。成本约 (1+k) 倍。 * 每行一条任务:采样 → 老师 → 距离下界认证过滤 → 局部时间律(50 Hz)→ 写出。 * 认证不过的样本也写出(keep=false + 原因),供 stats 统计丢弃率;训练只用 keep=true。 */ 'use strict'; @@ -9,6 +11,8 @@ const N = +args.n || 100, SEED = +args.seed || 1, OUT = args.out || 'ml/data/sha const { S, ArmIK } = require('../../tests/route_bench_setup.cjs')(); const sampler = require('./sampler.cjs')(S, SEED * 7919 + 13); const teach = require('./teacher.cjs')(S, ArmIK); +const { sameClass } = require('./pathdev.cjs'); +const K_CONS = +args.consensus || 0; const LEVER = S.leverBounds(4000); const cert = require('./cert.cjs')(S, { lever: LEVER }); const { timeParam } = require('./timelaw.cjs'); @@ -24,12 +28,26 @@ for (let i = 0; i < N; i++) { q0: task.q0.map(r3), T: task.T.map(r3), R: task.R, fp }; if (!t.ok) { rec.keep = false; rec.why = t.why; fs.writeSync(fd, JSON.stringify(rec) + '\n'); continue; } const c0 = performance.now(); - const c = cert.path(t.pts); + let c = null; + if (t.ptsJ6Frozen) { const cf = cert.path(t.ptsJ6Frozen); if (cf.ok) { t.pts = t.ptsJ6Frozen; t.travel = t.travelOf(t.pts); c = cf; rec.j6Frozen = true; } } + if (!c) c = cert.path(t.pts); + if (task.cat === 'D' && t.reached) rec.cat = 'Dr'; // "不可达"采样其实可达:单独记类,不当停靠样本 Object.assign(rec, { path: t.pts.map(q => q.map(r3)), reached: t.reached, err: r3(t.err), oerr: r3(t.oerr), travel_deg: r3(t.travel), plan_ms: Math.round(t.ms), blocked: t.blocked, poseRelaxed: t.poseRelaxed, detour: t.detour, cert: { ok: c.ok, queries: c.queries, minGap: r3(c.minGap), ms: Math.round(performance.now() - c0), why: c.why || null, failAt: c.failAt == null ? null : c.failAt } }); /* 训练可用:认证通过;D 类允许未到达(停靠),其余类必须到达;poseRelaxed 的 pose 样本降为 pos */ rec.keep = c.ok && (t.reached || task.cat === 'D'); + if (rec.keep && K_CONS > 0 && t.reached) { + let agree = 0, valid = 0; + for (let k = 0; k < K_CONS; k++) { + const u = [sampler.rnd() - 0.5, sampler.rnd() - 0.5, sampler.rnd() - 0.5], n = Math.hypot(u[0], u[1], u[2]); + const nb = teach(Object.assign({}, task, { T: task.T.map((v, j) => v + 5 * u[j] / n) })); + if (!nb.ok || !nb.reached) continue; + valid++; if (sameClass(t.pts, nb.ptsJ6Frozen && rec.j6Frozen ? nb.ptsJ6Frozen : nb.pts, task.mode)) agree++; + } + rec.consensus = { agree, valid }; + if (valid >= 2 && agree * 2 < valid) { rec.keep = false; rec.why = 'consensus'; } + } if (t.poseRelaxed && task.mode === 'pose') { rec.mode = 'pos'; rec.R = null; rec.demoted = true; rec.reached = t.err < 1; } if (rec.keep) { const tp = timeParam(t.pts); rec.traj = tp.traj.map(q => q.map(r3)); rec.exec_s = r3(tp.steps * 0.02); } fs.writeSync(fd, JSON.stringify(rec) + '\n'); diff --git a/ml/gen/pathdev.cjs b/ml/gen/pathdev.cjs new file mode 100644 index 0000000..3cbcebc --- /dev/null +++ b/ml/gen/pathdev.cjs @@ -0,0 +1,16 @@ +/* 路径偏差(同伦类判定用):两条展开折线按巡航时间 τ 各重采样 n 点,逐轴取最大差(°)。 + * 判定口径:pos 任务忽略 J6(位置任务里 J6 是冗余自由度),max < 30° 视为同一绕行类。 */ +'use strict'; +const { VMAX } = require('./timelaw.cjs'); +function resample(P, n) { + const seg = []; for (let i = 1; i < P.length; i++) { let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(P[i][j] - P[i - 1][j]) / VMAX[j]); seg.push(m); } + if (!seg.length) return Array.from({ length: n }, () => P[0].slice()); + const tot = seg.reduce((a, b) => a + b, 0) || 1; const out = []; + for (let k = 0; k < n; k++) { let tau = tot * k / (n - 1), i = 0; while (i < seg.length - 1 && tau > seg[i]) { tau -= seg[i]; i++; } + const u = seg[i] > 0 ? Math.min(1, tau / seg[i]) : 1; out.push(P[i].map((v, j) => v + (P[i + 1][j] - v) * u)); } + return out; +} +function devAx(A, B, n) { n = n || 32; const a = resample(A, n), b = resample(B, n); const m = [0, 0, 0, 0, 0, 0]; for (let k = 0; k < n; k++) for (let j = 0; j < 6; j++) m[j] = Math.max(m[j], Math.abs(a[k][j] - b[k][j])); return m; } +const SAME_DEG = 30; +function sameClass(A, B, mode) { const d = devAx(A, B); return Math.max.apply(null, mode === 'pos' ? d.slice(0, 5) : d) < SAME_DEG; } +module.exports = { resample, devAx, sameClass, SAME_DEG }; diff --git a/ml/gen/run_parallel.sh b/ml/gen/run_parallel.sh index 907ed8f..1146078 100755 --- a/ml/gen/run_parallel.sh +++ b/ml/gen/run_parallel.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# 并行生成:bash ml/gen/run_parallel.sh <每进程条数> <进程数> [起始种子=1] +# 并行生成:bash ml/gen/run_parallel.sh <每进程条数> <进程数> [起始种子=1] ["--consensus 4"] # 例:8 核机器 5 万条 → bash ml/gen/run_parallel.sh 6250 8 set -e -N=${1:-500}; P=${2:-2}; S0=${3:-1} +N=${1:-500}; P=${2:-2}; S0=${3:-1}; EXTRA=${4:-} cd "$(dirname "$0")/../.." mkdir -p ml/data -for ((i=0;i
ml/data/log_$s.txt & done +for ((i=0;i
ml/data/log_$s.txt & done wait node ml/gen/stats.cjs ml/data/shard_*.jsonl > ml/data/stats.json cat ml/data/stats.json diff --git a/ml/gen/stats.cjs b/ml/gen/stats.cjs index dcf4f21..78c4b6f 100644 --- a/ml/gen/stats.cjs +++ b/ml/gen/stats.cjs @@ -7,8 +7,8 @@ const q = a => { a.sort((x, y) => x - y); return a.length ? { p50: +a[a.length > const plan = [], certMs = [], travel = [], exec = [], gaps = []; for (const f of files) for (const line of fs.readFileSync(f, 'utf8').split('\n')) { if (!line) continue; const r = JSON.parse(line); total++; - const k = r.cat + '/' + r.mode; const b = by[k] = by[k] || { n: 0, reached: 0, certFail: 0, keep: 0, demoted: 0 }; - b.n++; if (r.reached) b.reached++; if (r.cert && !r.cert.ok) b.certFail++; if (r.demoted) b.demoted++; + const k = r.cat + '/' + r.mode; const b = by[k] = by[k] || { n: 0, reached: 0, certFail: 0, keep: 0, demoted: 0, consensusDrop: 0, j6Frozen: 0 }; + b.n++; if (r.reached) b.reached++; if (r.cert && !r.cert.ok) b.certFail++; if (r.demoted) b.demoted++; if (r.why === 'consensus') b.consensusDrop++; if (r.j6Frozen) b.j6Frozen++; if (r.plan_ms != null) plan.push(r.plan_ms); if (r.cert) certMs.push(r.cert.ms); if (r.cert) gaps.push(r.cert.minGap); if (r.keep) { b.keep++; keep++; travel.push(r.travel_deg); exec.push(r.exec_s); const n = r.traj.length - 1; steps += n; const s = Math.floor(n / 3) + 1; samples += s; diff --git a/ml/gen/teacher.cjs b/ml/gen/teacher.cjs index 6db8729..994a3a4 100644 --- a/ml/gen/teacher.cjs +++ b/ml/gen/teacher.cjs @@ -1,14 +1,19 @@ -/* 老师(方案 §4.3):solveDyn 松手口径——有离线路网时内部走 PRM 路网 Lazy A* + 捷径平滑(种子由起点决定,确定性), - * 路网失败再落到 RRT-Connect。返回展开后的折线与到达判据。 */ +/* 老师(方案 §4.3):solveDyn 松手口径——有离线路网时内部走 PRM 路网 Lazy A* + 捷径平滑,路网失败再落到 RRT-Connect。 + * 离线确定性(2026-10-17,M1 一致性门禁 78% 未过后加): + * - 每次求解前 resetSession():清空路网增广/坏边/已验段等跨调用状态,结果与样本顺序无关; + * - setDeterministic:毫秒预算改按碰撞判定次数计(msPerCheck 默认 0.5,即 300 ms 预算 ≈ 600 次判定),与机器负载无关; + * - pos 任务冻结 J6:位置任务里 J6 不影响 TCP,路径上 J6 恒取起点值(认证不过才保留老师原路径,由 gen 决定)。 */ 'use strict'; -module.exports = function makeTeacher(S, ArmIK) { +module.exports = function makeTeacher(S, ArmIK, opt) { + opt = opt || {}; const L = S.LIM, O = S.ORDER, w = S.wrap180; + if (S.setDeterministic) S.setDeterministic(true, opt.msPerCheck || 0.5); return function teach(task) { - const opt = { tool: 0, step: 0.5, ref: task.q0, from: task.q0.slice() }; - if (task.mode === 'pose') { opt.pose = { R: task.R }; opt.priority = 'pos'; } - if (S.rtReset) S.rtReset(); + const o = { tool: 0, step: 0.5, ref: task.q0, from: task.q0.slice() }; + if (task.mode === 'pose') { o.pose = { R: task.R }; o.priority = 'pos'; } + if (S.resetSession) S.resetSession(); else if (S.rtReset) S.rtReset(); const t0 = performance.now(); - const r = S.solveDyn(task.T, opt); + const r = S.solveDyn(task.T, o); const ms = performance.now() - t0; if (!r || !r.q) return { ok: false, ms, why: 'noq' }; const pts = [task.q0.slice()].concat(r.via || [], [r.q]); @@ -17,7 +22,13 @@ module.exports = function makeTeacher(S, ArmIK) { const err = Math.hypot(tcp[0] - task.T[0], tcp[1] - task.T[1], tcp[2] - task.T[2]); const oerr = task.mode === 'pose' ? ArmIK.rotErrDeg(S.orient(qf), task.R) : 0; const reached = err < 1 && oerr < 0.5 && !r.blocked; - let travel = 0; for (let i = 1; i < pts.length; i++) for (let k = 0; k < 6; k++) travel += Math.abs(pts[i][k] - pts[i - 1][k]); - return { ok: true, pts, reached, err, oerr, travel, ms, blocked: !!r.blocked, poseRelaxed: !!r.poseRelaxed, unsafe: !!r.unsafe, detour: r.detour || null }; + const travelOf = P => { let t = 0; for (let i = 1; i < P.length; i++) for (let k = 0; k < 6; k++) t += Math.abs(P[i][k] - P[i - 1][k]); return t; }; + const out = { ok: true, pts, reached, err, oerr, travel: travelOf(pts), ms, blocked: !!r.blocked, poseRelaxed: !!r.poseRelaxed, unsafe: !!r.unsafe, detour: r.detour || null }; + if (task.mode === 'pos') { + const j6 = task.q0[5]; + if (pts.some(p => Math.abs(p[5] - j6) > 1e-6)) out.ptsJ6Frozen = pts.map(p => { const c = p.slice(); c[5] = j6; return c; }); + } + out.travelOf = travelOf; + return out; }; }; diff --git a/webapp/js/ik-solver.js b/webapp/js/ik-solver.js index f35c973..8eb1a62 100644 --- a/webapp/js/ik-solver.js +++ b/webapp/js/ik-solver.js @@ -463,6 +463,14 @@ function make(cfg) { } return hit; } + function setDeterministic(on, msPerCheck) { VCLK = on ? { base: 0, per: msPerCheck || 0.5, tick: 0 } : null; } + /* 清空所有跨调用状态(路网增广、坏边/死边/已验段缓存、实时路径、全量 IK 缓存),使下一次求解与历史无关 */ + function resetSession() { + RMX.Q.length = 0; RMX.adj.length = 0; RMX.adjR = null; + BADE.clear(); DEAD.clear(); FINE.clear(); + RT = null; RTSTALL.key = null; RTSTALL.n = 0; + FULLIK.key = null; FULLIK.cur = null; FULLIK.T = null; FULLIK.at = 0; + } /* 全胶囊对最小间隙(mm,= 距离 − need,负值为干涉;≥cap 一律记 cap,下界、保守)(2026-10-17, ml/gen 距离下界认证用):不走 pair2d 查表、不提前退出,比 collideFree 慢,只给离线/安全层用。 */ function clearance(q, cap) { @@ -769,7 +777,11 @@ function make(cfg) { for (let i = 0; i < 6; i++) { const w = W ? W[i] : 1; t += Math.abs(wrap180(q[i] - ref[i])) * w; } return t + 1e-6 * c; }; - const nowMs = () => (typeof performance !== 'undefined' ? performance.now() : Date.now()); + /* 确定性时钟(2026-10-17,ml/gen 离线老师用):开启后各处毫秒预算改按"碰撞判定次数 × msPerCheck"计, + 另每次读时钟 +1 µs 防纯计算循环不前进。规划结果只取决于输入,不随机器快慢/负载变化。默认关闭。 */ + let VCLK = null; + const nowMs = () => VCLK ? VCLK.base + PST.n * VCLK.per + (VCLK.tick += 0.001) + : (typeof performance !== 'undefined' ? performance.now() : Date.now()); /* 姿态求解原语工厂:位置闭式支把 θ1..θ3 吸收掉之后,姿态任务只剩「工具轴指向 = 目标轴」这 2 个方程、2 个未知数 (θ4,θ5),解是孤立点,滚转再由 θ6 闭式补上。 @@ -3163,7 +3175,7 @@ function make(cfg) { solveRoute: solveRoute, routeFrom: routeFrom, transitPt: transitPt, planAround: planAround, edgeFree: edgeFree, PST: PST, unwrapTo: unwrapTo, rtWarm: rtWarm, rtReset: () => { RT = null; }, rmPlan: rmPlan, rmSmooth: rmSmooth, modelFingerprint: modelFingerprint, RM: RM, wd: wd, classGap: classGap, classFingerprint: classFingerprint, P2D: P2D, mixSolve: mixSolve, closest: closest, closest6: closest6, closestOri: closestOri, orient: orient, branch: branch, selfTest: selfTest, poseSelfTest: poseSelfTest, - collideFree: collideFree, collideDetail: collideDetail, clearance: clearance, gapMatrix: gapMatrix, leverBounds: leverBounds, + collideFree: collideFree, collideDetail: collideDetail, clearance: clearance, gapMatrix: gapMatrix, leverBounds: leverBounds, setDeterministic: setDeterministic, resetSession: resetSession, dynLimit: dynLimit, dynLimits: dynLimits, K: K, LIM: LIM, P: P, D: D, ORDER: ORDER, wrap180: wrap180, costOf: costOf }; }