Files

145 lines
8.4 KiB
Python
Raw Permalink Normal View History

"""老师轨迹回放自检(train 阶段开训前自动跑,约 1–3 分钟):
python -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json
两部分,都过才 pass(pipeline.sh 读 pass 字段与指纹,不过就停 rc 5):
1. 运动学口径(2026-10-18):gen 写出的老师 50 Hz 轨迹(keep=true、cat≠D)直接放进 PyTorch 运动学 / 碰撞 / 到达判据:
到达率 ≥ 0.95、碰撞率 ≤ 0.01。验证的是「仿真口径(kin.json、间隙、到达门槛)与 JS 认证过的数据一致」。
2. 滚动口径(2026-10-19,审查 review_closedloop F1/D1、review_data_actions C1):上面那条完全不经过 rollout(),
队列 / 前缀 / 提交 3 步 / qe / 标签 clamp / DLS / done / 时限上的错位它都发现不了。这里让老师当策略(closed_loop.TeacherOracle:
每次规划从当前时间下标取老师 16 步增量,按标签口径 clamp)原样走 rollout()(select='first'、use_sub=False):
到达(时限内)≥ 0.95、碰撞 ≤ 0.01、DLS 接管前与老师轨迹的最大关节偏差 p99 ≤ 0.1°(shard 轨迹按 1e-3° 取整,标签 clamp 会截掉取整的余量)。
输出另带 fp(当前 kin.json)、data_fp(shard 里记录的碰撞模型指纹)、ver=2;pipeline 三者都核对。
load_teacher / pad_traj / oracle_raw / oracle_summary 供 ml/eval/diag_cl.py 的 H1 段复用。"""
import argparse, glob, json, math, os, sys
import torch
from ml.common.kin_torch import Kin
from ml.sim.closed_loop import goal_err, rollout, TeacherOracle, DT
DEV_TOL_DEG = 0.1
def load_teacher(shards, ids):
"""按任务 id 从 shard jsonl 取老师记录(traj、path、exec_s…)。只解析 id 命中的行。"""
want, out = set(ids), {}
for f in sorted(glob.glob(shards)):
for line in open(f, encoding='utf-8'):
k = line.find('"id":"')
if k < 0: continue
rid = line[k + 6:line.find('"', k + 6)]
if rid in want and rid not in out:
r = json.loads(line)
if r.get('traj'): out[rid] = r
if len(out) == len(want): break
return out
def pad_traj(trajs, device, extra=20):
"""[B,Tmax+extra,6]:各题老师轨迹末帧重复补齐(oracle 在轨迹末尾之后给零增量)。"""
Tm = max(len(t) for t in trajs) + extra
out = torch.zeros(len(trajs), Tm, 6, device=device)
for i, t in enumerate(trajs):
x = torch.tensor(t, dtype=torch.float32, device=device)
out[i, :len(t)] = x; out[i, len(t):] = x[-1]
return out
def oracle_raw(per, TR, lens, tasks):
"""oracle 滚动的逐题量:DLS 接管前(且到达前、老师轨迹结束前)与老师的最大关节偏差 mdev(度)、时限内到达 ok、
时限内碰撞 col、是否计入到达统计(cat≠D)、完成时刻 − 老师时长 dt(s)、是否用过 DLS、2 s 目标误差比。"""
tr = per['traj'].to(TR.device); B, Tn, _ = tr.shape
L = min(Tn, TR.shape[1])
dev = (tr[:, :L] - TR[:, :L]).abs().max(-1).values # [B,L]
k = torch.arange(L, device=TR.device)[None]
kd = torch.tensor(lens, device=TR.device)[:, None] - 1
dls = per['dls_it'].to(TR.device)[:, None]
td = per['t_done'].to(TR.device)
kdone = torch.where(torch.isfinite(td), (td / DT).round(), torch.full_like(td, 1e9)).long()[:, None]
valid = (k <= kd) & ((dls < 0) | (k <= dls)) & (k <= kdone)
ex = torch.tensor([t.get('exec_s') or float('nan') for t in tasks])
return {'mdev': torch.where(valid, dev, torch.zeros_like(dev)).max(-1).values.cpu(), 'ok': per['ok'], 'col': per['col'] > 0,
'reach_m': torch.tensor([t.get('cat') != 'D' for t in tasks]), 'dt': per['t_done'] - ex,
'dls': per['dls_it'] >= 0, 'r2': per['goal_ratio_2s']}
def _q(v, p):
v = v[torch.isfinite(v)]
return float(v.kthvalue(max(1, min(len(v), int(math.ceil(p * len(v)))))).values) if len(v) else None
def oracle_summary(raws):
c = {k: torch.cat([r[k] for r in raws]) for k in raws[0]}
m = c['reach_m']; ok = c['ok']
dt = c['dt'][ok & m]
return {'n': int(len(m)), 'n_reach_tasks': int(m.sum()),
'reach': float(ok[m].float().mean()) if m.any() else None,
'collision': float(c['col'][m].float().mean()) if m.any() else None,
'D': {'n': int((~m).sum()), 'collision': float(c['col'][~m].float().mean()) if (~m).any() else None},
'max_dev_deg_before_dls': {'p50': _q(c['mdev'], .5), 'p99': _q(c['mdev'], .99), 'max': float(c['mdev'].max())},
'dls_used': float(c['dls'].float().mean()),
'finish_minus_teacher_s': {'p50': _q(dt, .5), 'p90': _q(dt, .9), 'max': float(dt.max()) if len(dt) else None},
'goal_ratio_2s_p50': _q(c['r2'], .5)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--shards', default='ml/data/shard_*.jsonl'); ap.add_argument('--n', type=int, default=512)
ap.add_argument('--samples', default='ml/data/samples', help='读打包 meta 的 act_scale(oracle 按同一标签口径 clamp);没有就按 1.0')
ap.add_argument('--bs', type=int, default=128)
ap.add_argument('--out', default='ml/runs/replay_teacher.json')
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
kin = Kin(device=a.device)
recs = []
for f in sorted(glob.glob(a.shards)):
for line in open(f, encoding='utf-8'):
r = json.loads(line)
if r.get('keep') and r.get('traj') and r.get('cat') != 'D': recs.append(r)
if len(recs) >= a.n: break
if len(recs) >= a.n: break
if not recs: sys.exit('没有可回放的老师轨迹(先跑 gen)')
# ---- 1. 运动学口径
reach = col = 0; worst_gap = []
for r in recs:
q = torch.tensor(r['traj'], dtype=torch.float32, device=a.device)
mid = 0.5 * (q[1:] + q[:-1])
g = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
hit = bool((g <= 0).any()); col += hit; worst_gap.append(g.min().item())
T = torch.tensor([r['T']], dtype=torch.float32, device=a.device)
R = torch.tensor([r['R'] if r.get('R') else [0.] * 9], dtype=torch.float32, device=a.device)
e, oe = goal_err(kin, q[-1:], T, R)
reach += bool((e < 1).item() and (oe < 0.5).item())
n = len(recs)
kin_ok = reach / n >= 0.95 and col / n <= 0.01
# ---- 2. 滚动口径:老师 oracle 走完整 rollout()
try:
from ml.data import read_meta
act_scale = float(read_meta(a.samples).get('act_scale', 1.0)) if os.path.isdir(a.samples) else 1.0
except SystemExit:
act_scale = 1.0
tasks = [{'q0': r['q0'], 'T': r['T'], 'R': r.get('R'), 'exec_s': r.get('exec_s') or (len(r['traj']) - 1) * DT,
'travel': r.get('travel_deg'), 'cat': r.get('cat')} for r in recs]
raws = []
for i in range(0, n, a.bs):
tb, rb = tasks[i:i + a.bs], recs[i:i + a.bs]
TR = pad_traj([r['traj'] for r in rb], a.device)
_, per = rollout(None, kin, tb, a.device, K=1, select='first', use_sub=False, return_traj=True, seed=i,
oracle=TeacherOracle(TR, act_scale=act_scale))
raws.append(oracle_raw(per, TR, [len(r['traj']) for r in rb], tb))
orc = oracle_summary(raws); orc['act_scale'] = act_scale
orc_ok = (orc['reach'] or 0) >= 0.95 and (orc['collision'] or 0) <= 0.01 and orc['max_dev_deg_before_dls']['p99'] <= DEV_TOL_DEG
data_fps = {r.get('fp') for r in recs}
data_fp = data_fps.pop() if len(data_fps) == 1 else 'mixed'
res = {'ver': 2, 'n': n, 'reach': reach / n, 'collision': col / n, 'min_gap_mm_p10': sorted(worst_gap)[n // 10], 'kin_pass': kin_ok,
'oracle': orc, 'oracle_pass': orc_ok, 'fp': kin.fp, 'data_fp': data_fp,
'pass': bool(kin_ok and orc_ok and data_fp in (None, kin.fp))}
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
json.dump(res, open(a.out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
print('老师轨迹回放:到达 %.1f%%、碰撞 %.1f%%(%d 条);oracle 走 rollout:到达 %.1f%%、碰撞 %.1f%%、DLS 前最大偏差 p99 %.4f°;指纹 %s' % (
100 * res['reach'], 100 * res['collision'], n, 100 * orc['reach'], 100 * orc['collision'],
orc['max_dev_deg_before_dls']['p99'], '一致' if data_fp in (None, kin.fp) else '不符'), flush=True)
sys.exit(0 if res['pass'] else 5)
if __name__ == '__main__':
main()