Files
Robot_Arm/ml/eval/diag_tail.py
T

129 lines
6.0 KiB
Python
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.
#!/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
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
model, ck = load_policy(a.ckpt, a.device)
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()