2026-10-18 23:15:53 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
"""闭环末位误差诊断(只读,手动运行;不在流水线里)。
|
|
|
|
|
|
|
|
|
|
|
|
回答一个问题:策略"差一点没进 1 mm 门"还是"根本没朝目标走"。
|
|
|
|
|
|
输出每题:
|
|
|
|
|
|
- 最终位置误差 err_mm / 最终姿态误差 oerr_deg
|
|
|
|
|
|
- 全程最小位置误差 min_err_mm、最小综合误差时是否进过 5 mm & 5° 窗口(DLS 接管门槛)
|
|
|
|
|
|
- done(是否到达)、碰撞步数、用时
|
|
|
|
|
|
以及按 cat/mode 分组的汇总与分位数。
|
|
|
|
|
|
|
|
|
|
|
|
用法(仓库根目录):
|
|
|
|
|
|
PYTHONUTF8=1 python ml/eval/diag_tail.py --ckpt ml/runs/base/last.pt --n 512 --out ml/runs/diag_tail.json
|
|
|
|
|
|
"""
|
|
|
|
|
|
import argparse, json, math, os, sys
|
|
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
|
|
|
|
|
|
|
|
import torch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pct(v, p):
|
|
|
|
|
|
if not v:
|
|
|
|
|
|
return None
|
|
|
|
|
|
v = sorted(v)
|
|
|
|
|
|
return v[min(len(v) - 1, int(len(v) * p))]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
ap = argparse.ArgumentParser()
|
|
|
|
|
|
ap.add_argument('--ckpt', required=True)
|
|
|
|
|
|
ap.add_argument('--samples', default='ml/data/samples')
|
|
|
|
|
|
ap.add_argument('--tasks', default=None, help='任务 jsonl(覆盖 --samples/--split)')
|
|
|
|
|
|
ap.add_argument('--split', default='val')
|
|
|
|
|
|
ap.add_argument('--n', type=int, default=512)
|
|
|
|
|
|
ap.add_argument('--bs', type=int, default=64, help='每批题数(显存小就调小)')
|
|
|
|
|
|
ap.add_argument('--K', type=int, default=8)
|
|
|
|
|
|
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
|
|
|
|
|
ap.add_argument('--out', default=None)
|
|
|
|
|
|
a = ap.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
from ml.common.kin_torch import Kin, D2R
|
|
|
|
|
|
from ml.data import load_tasks
|
|
|
|
|
|
from ml.model.policy import build
|
|
|
|
|
|
from ml.sim.closed_loop import rollout, goal_err
|
|
|
|
|
|
|
2026-10-19 10:43:05 +00:00
|
|
|
|
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
|
|
|
|
|
|
model, ck = load_policy(a.ckpt, a.device)
|
2026-10-18 23:15:53 +08:00
|
|
|
|
kin = Kin(device=a.device)
|
|
|
|
|
|
if ck.get('fp') and ck['fp'] != kin.fp:
|
|
|
|
|
|
sys.exit('checkpoint 的碰撞模型指纹 %s 与当前 kin.json %s 不符' % (ck['fp'], kin.fp))
|
|
|
|
|
|
|
|
|
|
|
|
if a.tasks:
|
|
|
|
|
|
tasks = [json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()]
|
|
|
|
|
|
else:
|
|
|
|
|
|
tasks = [t for t in load_tasks([a.samples]) if t['split'] == a.split]
|
|
|
|
|
|
tasks = tasks[:a.n]
|
|
|
|
|
|
|
|
|
|
|
|
per_task = []
|
|
|
|
|
|
with torch.no_grad():
|
|
|
|
|
|
for i in range(0, len(tasks), a.bs):
|
|
|
|
|
|
chunk = tasks[i:i + a.bs]
|
|
|
|
|
|
res, per = rollout(model, kin, chunk, a.device, K=a.K, safety=False, seed=i, return_traj=True)
|
|
|
|
|
|
traj = per['traj'] # [B, T, 6]
|
|
|
|
|
|
B, Tn, _ = traj.shape
|
|
|
|
|
|
Tt = torch.tensor([t['T'] for t in chunk], dtype=torch.float32, device=a.device)
|
|
|
|
|
|
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in chunk], dtype=torch.float32, device=a.device)
|
|
|
|
|
|
min_e = torch.full((B,), float('inf'), device=a.device)
|
|
|
|
|
|
min_o = torch.full((B,), float('inf'), device=a.device)
|
|
|
|
|
|
entered = torch.zeros(B, dtype=torch.bool, device=a.device) # 曾同时满足 e<5mm & (无姿态要求或 oe<5°)
|
|
|
|
|
|
step = 256
|
|
|
|
|
|
for t0 in range(0, Tn, step):
|
|
|
|
|
|
q = traj[:, t0:t0 + step].reshape(-1, 6)
|
|
|
|
|
|
n = q.shape[0] // B
|
|
|
|
|
|
e, oe = goal_err(kin, q, Tt.repeat_interleave(n, 0), Rt.repeat_interleave(n, 0))
|
|
|
|
|
|
e = e.view(B, n); oe = oe.view(B, n)
|
|
|
|
|
|
min_e = torch.minimum(min_e, e.min(1).values)
|
|
|
|
|
|
min_o = torch.minimum(min_o, oe.min(1).values)
|
|
|
|
|
|
pose = (Rt.abs().sum(-1) > 0)[:, None]
|
|
|
|
|
|
window = (e < 5) & (~pose | (oe < 5))
|
|
|
|
|
|
entered |= window.any(1)
|
|
|
|
|
|
done = per['done'].to(a.device); col = per['col'].to(a.device); td = per['t_done'].to(a.device)
|
|
|
|
|
|
end_e = per['err'].to(a.device)
|
|
|
|
|
|
# 末态姿态误差:traj 最后一帧
|
|
|
|
|
|
qend = traj[:, -1]
|
|
|
|
|
|
ee, eo = goal_err(kin, qend, Tt, Rt)
|
|
|
|
|
|
for j, t in enumerate(chunk):
|
|
|
|
|
|
per_task.append({
|
|
|
|
|
|
'tid': t.get('tid'), 'cat': t.get('cat'), 'mode': t.get('mode'),
|
|
|
|
|
|
'err_mm': float(end_e[j]), 'oerr_deg': float(eo[j]),
|
|
|
|
|
|
'min_err_mm': float(min_e[j]), 'min_oerr_deg': float(min_o[j]),
|
|
|
|
|
|
'entered_5mm5deg': bool(entered[j]),
|
|
|
|
|
|
'done': bool(done[j]), 'collision_steps': float(col[j]),
|
|
|
|
|
|
't_done_s': None if math.isnan(float(td[j])) else float(td[j]),
|
|
|
|
|
|
})
|
|
|
|
|
|
print('[diag] %d/%d done=%d entered=%d min_err<=5mm=%d' % (
|
|
|
|
|
|
min(i + a.bs, len(tasks)), len(tasks), int(done.sum()), int(entered.sum()),
|
|
|
|
|
|
sum(1 for p in per_task if p['min_err_mm'] <= 5)), flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
def summ(rows):
|
|
|
|
|
|
if not rows:
|
|
|
|
|
|
return {'n': 0}
|
|
|
|
|
|
e = [r['err_mm'] for r in rows]; me = [r['min_err_mm'] for r in rows]
|
|
|
|
|
|
o = [r['oerr_deg'] for r in rows]
|
|
|
|
|
|
return {'n': len(rows),
|
|
|
|
|
|
'done': sum(r['done'] for r in rows),
|
|
|
|
|
|
'entered_5mm5deg': sum(r['entered_5mm5deg'] for r in rows),
|
|
|
|
|
|
'min_err_mm<=5': sum(1 for v in me if v <= 5),
|
|
|
|
|
|
'min_err_mm<=10': sum(1 for v in me if v <= 10),
|
|
|
|
|
|
'err_mm_p10/p50/p90': [pct(e, .1), pct(e, .5), pct(e, .9)],
|
|
|
|
|
|
'min_err_mm_p10/p50/p90': [pct(me, .1), pct(me, .5), pct(me, .9)],
|
|
|
|
|
|
'oerr_deg_p50/p90': [pct(o, .5), pct(o, .9)],
|
|
|
|
|
|
'collision_steps_mean': sum(r['collision_steps'] for r in rows) / len(rows)}
|
|
|
|
|
|
|
|
|
|
|
|
out = {'ckpt': a.ckpt, 'step': ck.get('step'), 'fp': ck.get('fp'), 'n': len(per_task),
|
|
|
|
|
|
'overall': summ(per_task),
|
|
|
|
|
|
'by_cat_mode': {k: summ([r for r in per_task if '%s/%s' % (r['cat'], r['mode']) == k])
|
|
|
|
|
|
for k in sorted({('%s/%s' % (r['cat'], r['mode'])) for r in per_task})},
|
|
|
|
|
|
'tasks': per_task}
|
|
|
|
|
|
s = json.dumps(out, indent=1, ensure_ascii=False)
|
|
|
|
|
|
print(s[:2000])
|
|
|
|
|
|
if a.out:
|
|
|
|
|
|
open(a.out + '.tmp', 'w', encoding='utf-8').write(s)
|
|
|
|
|
|
os.replace(a.out + '.tmp', a.out)
|
|
|
|
|
|
print('[diag] 已写', a.out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
main()
|