2026-10-17 15:38:00 +00:00
|
|
|
|
"""闭环仿真(验证 / 挖错 / 门禁共用,PyTorch 批量,无梯度)——按部署节奏运行:
|
|
|
|
|
|
50 Hz 执行;队列 ≤ 3 步时推理一次(已提交前缀 r = 队列长度),提交接下来 3 步;
|
2026-10-19 10:43:05 +00:00
|
|
|
|
冷启动用价值头对候选末态打分(短前瞻),之后按与上一条**真正提交**的计划的一致性选候选;
|
|
|
|
|
|
接近目标 < 5 mm 时用局部 DLS 精修;子目标头在新目标时给出途经点,按「越过顶点」规则切下一个(与标签同口径,见 sub_advance)。
|
2026-10-17 15:38:00 +00:00
|
|
|
|
safety=False 时不做任何碰撞拦截(暴露真实错误,挖错用);safety=True 时近似部署安全层(候选提交步有干涉则换下一条,全不过则停一步)。
|
2026-10-19 10:43:05 +00:00
|
|
|
|
动作反归一化:inc = clamp(act, −1, 1) · act_scale · STEP;act_scale 来自 checkpoint(旧模型缺省 1.0,见 ml/common/ckpt.load_policy)。
|
|
|
|
|
|
碰撞与到达只在每题自己的时限内计(短题与长题同批时不把超时后的部分算进去)。
|
2026-10-17 15:38:00 +00:00
|
|
|
|
python -m ml.sim.closed_loop --ckpt ml/runs/base/best.pt --samples ml/data/samples --split val --n 1000 [--safety]"""
|
|
|
|
|
|
import argparse, json, math, os
|
|
|
|
|
|
import torch
|
|
|
|
|
|
|
|
|
|
|
|
from ml.common.kin_torch import Kin
|
2026-10-19 10:43:05 +00:00
|
|
|
|
from ml.common.features import features, STEP, VMAX, H, GAP_CAP, D2R, wrap180
|
2026-10-17 15:38:00 +00:00
|
|
|
|
|
|
|
|
|
|
DT = 0.02
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-17 18:57:48 +00:00
|
|
|
|
FIRST_DEG = 5.0 # R7 首次方向判定阈值(度):小于它的抖动不算选边
|
2026-10-19 10:43:05 +00:00
|
|
|
|
SUB_LEAD_S = 0.02 # 子目标「越过顶点」提前量(s):标签在老师 τ > τ_顶点 − 0.02 时就换到下一个顶点(build_dataset.cjs remain 过滤)
|
|
|
|
|
|
SUB_REACH_DEG, SUB_TIMEOUT_S = 5.0, 1.5 # 旧规则(sub_rule='timeout'):到达 < 5° 或 1.5 s 超时
|
2026-10-17 18:57:48 +00:00
|
|
|
|
|
2026-10-17 15:38:00 +00:00
|
|
|
|
def tasks_to_tensors(tasks, device):
|
|
|
|
|
|
q0 = torch.tensor([t['q0'] for t in tasks], dtype=torch.float32, device=device)
|
|
|
|
|
|
T = torch.tensor([t['T'] for t in tasks], dtype=torch.float32, device=device)
|
|
|
|
|
|
R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tasks], dtype=torch.float32, device=device)
|
2026-10-17 16:35:52 +00:00
|
|
|
|
# 无老师参考(挖错/门禁题)时 exec_s/travel 为 None → NaN;调用方据此区分口径
|
|
|
|
|
|
ex = torch.tensor([t['exec_s'] if t.get('exec_s') else float('nan') for t in tasks], dtype=torch.float32, device=device)
|
|
|
|
|
|
tr = torch.tensor([t['travel'] if t.get('travel') else float('nan') for t in tasks], dtype=torch.float32, device=device)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
return q0, T, R, ex, tr
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-17 16:35:52 +00:00
|
|
|
|
NOREF_SEC = 6.0 # 无老师参考题的定长时限(s)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-19 10:43:05 +00:00
|
|
|
|
def time_limit(ex, max_sec=None):
|
|
|
|
|
|
"""每题时限(s):有老师 = 老师时长 × 2(至少 3 s);无老师 = NOREF_SEC;max_sec 覆盖全部。"""
|
|
|
|
|
|
if max_sec is not None: return torch.full_like(ex, max_sec)
|
|
|
|
|
|
return torch.where(torch.isfinite(ex), torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-17 15:38:00 +00:00
|
|
|
|
def goal_err(kin, q, T, R):
|
|
|
|
|
|
xf = kin.cum_xf(q)
|
|
|
|
|
|
p = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(-1, 9)
|
|
|
|
|
|
e = (T - p).norm(dim=-1)
|
|
|
|
|
|
pose = R.abs().sum(-1) > 0
|
|
|
|
|
|
tr = (Rq * R).sum(-1)
|
|
|
|
|
|
oe = torch.where(pose, torch.acos(((tr - 1) / 2).clamp(-1, 1)) / D2R, torch.zeros_like(e))
|
|
|
|
|
|
return e, oe
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-20 17:26:20 +08:00
|
|
|
|
def execution_points(kin, q, increments):
|
|
|
|
|
|
points, midpoints = [], []
|
|
|
|
|
|
for delta in increments.unbind(1):
|
|
|
|
|
|
nxt = kin.clamp_box(q + delta)
|
|
|
|
|
|
midpoints.append(q + 0.5 * (nxt - q))
|
|
|
|
|
|
points.append(nxt)
|
|
|
|
|
|
q = nxt
|
|
|
|
|
|
return torch.stack(points, 1), torch.stack(midpoints, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-20 13:33:23 +08:00
|
|
|
|
def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5, analytic=True):
|
|
|
|
|
|
"""局部 DLS 精修:n 步增量 [B,n,6](analytic:几何雅可比,1 次 FK;否则 7 次批量有限差分)"""
|
2026-10-17 15:38:00 +00:00
|
|
|
|
step = torch.tensor(STEP, device=q.device)
|
2026-10-20 12:20:31 +08:00
|
|
|
|
B = q.shape[0]
|
2026-10-17 15:38:00 +00:00
|
|
|
|
pose = (R.abs().sum(-1) > 0).float()
|
|
|
|
|
|
Rt = R.view(-1, 3, 3)
|
2026-10-20 13:33:23 +08:00
|
|
|
|
def err(x, Tb, Rtb, poseb, xf=None):
|
|
|
|
|
|
xf = xf if xf is not None else kin.cum_xf(x)
|
|
|
|
|
|
p = kin.fk(x, xf); Rq = kin.orient(x, xf)
|
2026-10-20 12:20:31 +08:00
|
|
|
|
M = Rtb @ Rq.transpose(1, 2)
|
|
|
|
|
|
rv = 0.5 * torch.stack([M[:, 2, 1] - M[:, 1, 2], M[:, 0, 2] - M[:, 2, 0], M[:, 1, 0] - M[:, 0, 1]], -1) * 100 * poseb[:, None]
|
|
|
|
|
|
return torch.cat([Tb - p, rv], -1)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
out = []; x = q.clone()
|
2026-10-20 12:20:31 +08:00
|
|
|
|
ar = torch.arange(6, device=q.device)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
for _ in range(n):
|
2026-10-20 13:33:23 +08:00
|
|
|
|
xf = kin.cum_xf(x)
|
|
|
|
|
|
e0 = err(x, T, Rt, pose, xf)
|
|
|
|
|
|
if analytic:
|
2026-10-20 17:06:53 +08:00
|
|
|
|
# 几何角速度 Jw 仅在姿态误差接近零时等于该 skew 残差的负导数。
|
2026-10-20 13:33:23 +08:00
|
|
|
|
Jv, Jw = kin.jac(x, xf)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
J = torch.cat([Jv, 100 * Jw * pose[:, None, None]], 1)
|
2026-10-20 13:33:23 +08:00
|
|
|
|
else:
|
|
|
|
|
|
dx = torch.zeros(B, 6, 6, device=q.device); dx[:, ar, ar] = 1e-3
|
|
|
|
|
|
xp = (x[:, None, :] + dx).reshape(B * 6, 6)
|
|
|
|
|
|
ep = err(xp, T.repeat_interleave(6, 0), Rt.repeat_interleave(6, 0), pose.repeat_interleave(6, 0))
|
|
|
|
|
|
J = -((ep - e0.repeat_interleave(6, 0)) / 1e-3).view(B, 6, 6).transpose(1, 2) # [B,6(err),6(q)]
|
2026-10-17 15:38:00 +00:00
|
|
|
|
A = J @ J.transpose(1, 2) + lam ** 2 * torch.eye(6, device=q.device)
|
|
|
|
|
|
y = torch.linalg.solve(A, e0.unsqueeze(-1))
|
|
|
|
|
|
dq = (J.transpose(1, 2) @ y).squeeze(-1)
|
|
|
|
|
|
sc = (dq.abs() / (step * scale)).max(-1, keepdim=True).values.clamp(min=1)
|
|
|
|
|
|
dq = dq / sc
|
|
|
|
|
|
out.append(dq); x = x + dq
|
|
|
|
|
|
return torch.stack(out, 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-19 10:43:05 +00:00
|
|
|
|
def jdiff(kin, a, b):
|
|
|
|
|
|
"""a − b(度),连续轴取最短弧。"""
|
|
|
|
|
|
d = a - b
|
|
|
|
|
|
return torch.where(kin.cont, wrap180(d), d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sub_advance(kin, subs, nsub, sub_prev, sub_t, qe, tnow, rule='cross'):
|
|
|
|
|
|
"""子目标推进(每次规划前调用,每次最多推进一个)。subs [B,M,6] 待走途经点(绝对角),nsub [B] 剩余个数,
|
|
|
|
|
|
sub_prev [B,6] 上一个已越过的点(冷启动时 = 起点),sub_t [B] 当前子目标开始时刻。返回 (subs, nsub, sub_prev, sub_t)。
|
|
|
|
|
|
rule='cross'(默认):qe 沿拐角平分面法向越过当前顶点(提前 SUB_LEAD_S 秒)即切到下一个。
|
|
|
|
|
|
2026-10-19(审查 review_data_actions C5 / review_model_train P0-1):标签里 sub = 老师「下一个尚未经过的顶点」,
|
|
|
|
|
|
老师 τ 一过顶点(−0.02 s)立刻换;旧规则要等最大关节差 < 5° 或 1.5 s 超时,策略在顶点附近 5–10° 进不去时
|
|
|
|
|
|
sub 一直指着身后的点,这种「sub 已在身后」的特征序列训练里从未出现。运行时没有老师 τ,用几何等价物:
|
|
|
|
|
|
进入方向 a = 顶点 − 上一点、离开方向 b = 下一顶点 − 顶点(最后一个顶点取 a),各轴按 VMAX 折成秒(与 τ 同量纲),
|
|
|
|
|
|
法向 n = â + b̂;(qe − 顶点)·n̂ ≥ −SUB_LEAD_S 判为越过。没有超时(标签也没有)。
|
|
|
|
|
|
rule='timeout':旧部署口径,到达 < 5° 或 1.5 s 超时(诊断对照用)。
|
|
|
|
|
|
ik-policy.js Runner(subRule)与 apg_finetune.py 用同一规则,三处同步改。"""
|
|
|
|
|
|
has = nsub > 0
|
|
|
|
|
|
if not bool(has.any()): return subs, nsub, sub_prev, sub_t
|
|
|
|
|
|
cur = subs[:, 0]
|
|
|
|
|
|
if rule == 'timeout':
|
|
|
|
|
|
dd = jdiff(kin, cur, qe).abs().max(-1).values
|
|
|
|
|
|
adv = has & ((dd < SUB_REACH_DEG) | (tnow - sub_t > SUB_TIMEOUT_S))
|
|
|
|
|
|
elif rule == 'cross':
|
|
|
|
|
|
vmax = torch.tensor(VMAX, device=qe.device, dtype=qe.dtype)
|
|
|
|
|
|
un = lambda v: v / v.norm(dim=-1, keepdim=True).clamp(min=1e-9)
|
|
|
|
|
|
a = jdiff(kin, cur, sub_prev) / vmax
|
|
|
|
|
|
nxt = subs[:, 1] if subs.shape[1] > 1 else cur
|
|
|
|
|
|
b = torch.where((nsub > 1)[:, None], jdiff(kin, nxt, cur) / vmax, a)
|
|
|
|
|
|
n = un(un(a) + un(b))
|
|
|
|
|
|
proj = (jdiff(kin, qe, cur) / vmax * n).sum(-1)
|
|
|
|
|
|
adv = has & (proj >= -SUB_LEAD_S)
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError('sub_rule: ' + str(rule))
|
|
|
|
|
|
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, -1:]], 1), subs)
|
|
|
|
|
|
sub_prev = torch.where(adv[:, None], cur, sub_prev)
|
|
|
|
|
|
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
|
|
|
|
|
|
return subs, nsub, sub_prev, sub_t
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TeacherOracle:
|
|
|
|
|
|
"""老师当策略(2026-10-19,审查 review_closedloop D1 / review_data_actions §4.1):
|
|
|
|
|
|
规划时从当前时间下标 it 取老师 50 Hz 轨迹的 16 步增量,按与 build_dataset.cjs 相同的标签口径
|
|
|
|
|
|
act = clamp(Δ / (act_scale·STEP), −1, 1) 交给 rollout(),其余(队列、前缀、提交、qe、DLS、done、碰撞、时限)一行不改。
|
|
|
|
|
|
traj [B,Tmax,6]:每题老师轨迹,末帧重复补齐(按 id 从 ml/data/shard_*.jsonl 取)。
|
|
|
|
|
|
sigma > 0:每次规划给 act 加 N(0, σ²)(σ 以 STEP 为单位,各候选独立),测闭环对残差噪声的敏感度。
|
|
|
|
|
|
跟随时第 it 步的 q 应等于 traj[:, it](DLS 接管或到达冻结之后除外)。"""
|
|
|
|
|
|
head = 'oracle'
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, traj, act_scale=1.0, sigma=0.0):
|
|
|
|
|
|
self.traj, self.act_scale, self.sigma = traj, float(act_scale), float(sigma)
|
|
|
|
|
|
|
|
|
|
|
|
def plan(self, it, K, g):
|
|
|
|
|
|
B, Tm, _ = self.traj.shape
|
|
|
|
|
|
dev = self.traj.device
|
|
|
|
|
|
idx = (it + torch.arange(H + 1, device=dev)).clamp(max=Tm - 1)
|
|
|
|
|
|
seg = self.traj[:, idx]
|
|
|
|
|
|
astep = torch.tensor(STEP, device=dev) * self.act_scale
|
|
|
|
|
|
act = ((seg[:, 1:] - seg[:, :-1]) / astep).clamp(-1, 1)
|
|
|
|
|
|
act = act[:, None].expand(B, K, H, 6).contiguous()
|
|
|
|
|
|
if self.sigma > 0:
|
|
|
|
|
|
act = act + torch.randn(B, K, H, 6, device=dev, generator=g) * (self.sigma / self.act_scale)
|
|
|
|
|
|
return act
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-17 15:38:00 +00:00
|
|
|
|
@torch.no_grad()
|
2026-10-19 10:43:05 +00:00
|
|
|
|
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None,
|
2026-10-20 03:09:21 +08:00
|
|
|
|
qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross',
|
2026-10-20 12:38:56 +08:00
|
|
|
|
fb_gain=0.0, fb_scale=0.0, fb_near=0.2, fb_fallback=0.0, safety_partial=0, commit_n=3, collect=None,
|
2026-10-20 17:06:53 +08:00
|
|
|
|
fb_exec=0.0, exec_safety=0, fb_ki=0.0, fb_decay=0.9, fb_imax=0.5, fb_every=1, safety_try=0, sel_w=0.1, dls_safety=0, fb_clear=0.0, fb_cmin=0.3, sub_refresh=0, fb_block_gain=1.0,
|
2026-10-20 22:05:12 +08:00
|
|
|
|
inc_cap=0, gap_margin=0.0, cert_batch=0, backoff=0, len_w=0.0, progress=None):
|
2026-10-19 10:43:05 +00:00
|
|
|
|
"""use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头;
|
|
|
|
|
|
qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放;
|
|
|
|
|
|
select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。
|
|
|
|
|
|
oracle:TeacherOracle,代替 model 出动作(model 可为 None,须配 select='first'、use_sub=False)。
|
|
|
|
|
|
replan:'r3'(部署:队列 ≤ 3 步就规划,稳态 r=3)/ 'empty'(队列空了才规划,每次 r=0,前缀消融用)。
|
2026-10-20 19:48:23 +08:00
|
|
|
|
sub_rule:子目标推进规则,见 sub_advance。除默认值外均为诊断用。
|
|
|
|
|
|
inc_cap=1:把「策略动作 + 目标反馈」的合计增量再按每轴 VMAX·DT 限一次幅(默认关;关时反馈是在 ±1 限幅之后叠加的,第30轮实测最大 1.66×VMAX)。
|
|
|
|
|
|
gap_margin>0:安全层要求间隙大于该毫米余量而不是 >0;宽相位 cap 自动抬到 margin+0.5,否则 0.5 的截顶值会让余量门限恒假。
|
2026-10-20 21:25:47 +08:00
|
|
|
|
backoff>0:全拒(连第 1 步都不安全)时不再站住,把首选候选按 1/2、1/4…(共 backoff 档)缩步一起送进同一次认证,
|
|
|
|
|
|
取能通过的第 1 档执行;缩步候选不参与"前缀更长者优先"的比较,只在原候选全被拒时顶上。需 cert_batch=1。
|
2026-10-20 22:05:12 +08:00
|
|
|
|
select='ep_len' + len_w>0:在末态误差打分上再减 len_w·Σ|Δ|/STEP(每轴归一的候选累计行程)。§9.2 要行程≤规划器×1.10,
|
|
|
|
|
|
而实测行程比 2.4~2.6 没有任何机制在管;len_w=0 与 select='endpoint' 逐位相同(有回归断言)。
|
2026-10-20 19:48:23 +08:00
|
|
|
|
两者默认值下行为与第32轮逐位相同。"""
|
2026-10-20 21:25:47 +08:00
|
|
|
|
if backoff and not cert_batch: raise ValueError('backoff 目前只实现在 cert_batch=1 口径')
|
2026-10-17 15:38:00 +00:00
|
|
|
|
g = torch.Generator(device=device); g.manual_seed(seed)
|
|
|
|
|
|
q0, T, R, ex, tr = tasks_to_tensors(tasks, device)
|
|
|
|
|
|
B = q0.shape[0]
|
|
|
|
|
|
step = torch.tensor(STEP, device=device)
|
2026-10-20 19:48:23 +08:00
|
|
|
|
gcap = max(0.5, gap_margin + 0.5) # 余量门限必须落在截顶值之内才有意义
|
2026-10-19 10:43:05 +00:00
|
|
|
|
act_scale = oracle.act_scale if oracle is not None else float(getattr(model, 'act_scale', 1.0))
|
|
|
|
|
|
astep = step * act_scale # 动作反归一化:每单位 act 对应的度数
|
2026-10-17 16:35:52 +00:00
|
|
|
|
has_ref = torch.isfinite(ex)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
limit = time_limit(ex, max_sec)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
nmax = int(math.ceil(limit.max().item() / DT))
|
2026-10-18 18:53:39 +00:00
|
|
|
|
q, qp = q0.clone(), (qp0.to(device) if qp0 is not None else q0.clone())
|
2026-10-17 15:38:00 +00:00
|
|
|
|
queue = torch.zeros(B, 0, 6, device=device)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
last = None; last_ok = torch.zeros(B, dtype=torch.bool, device=device)
|
2026-10-20 13:33:23 +08:00
|
|
|
|
fb_i = torch.zeros(B, 6, device=device) # 积分项:累积持续同向的目标反馈(慢变偏差)
|
2026-10-20 14:23:59 +08:00
|
|
|
|
fb_cache = None; pit = 0 # fb_every>1:复用上一次规划的目标反馈(误差方向慢变)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
last_brake = torch.zeros(B, dtype=torch.bool, device=device)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device)
|
|
|
|
|
|
sub_t = torch.zeros(B, device=device); sub_prev = q0.clone()
|
2026-10-17 15:38:00 +00:00
|
|
|
|
done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device)
|
|
|
|
|
|
col = torch.zeros(B, device=device); travel = torch.zeros(B, device=device); brakes = torch.zeros(B, device=device)
|
2026-10-20 21:25:47 +08:00
|
|
|
|
bo = torch.zeros(B, device=device) # 缩步顶上的次数(backoff>0 才会计)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
first_hit = torch.full((B,), float('nan'), device=device); dls_it = torch.full((B,), -1, dtype=torch.long, device=device)
|
2026-10-20 19:48:23 +08:00
|
|
|
|
# 2026-10-20(轮次33):速度/加速度按「实际执行位移」算,不看指令——第30轮证明反馈是在限幅之后叠加的
|
|
|
|
|
|
vratio = torch.zeros(B, device=device); aratio = torch.zeros(B, device=device); prev_exec = torch.zeros(B, 6, device=device)
|
2026-10-20 22:26:34 +08:00
|
|
|
|
rev = torch.zeros(B, device=device); nact = torch.zeros(B, device=device); prev_sgn = torch.zeros(B, 6, device=device)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
traj = [q.clone()] if return_traj else None
|
2026-10-20 21:16:27 +08:00
|
|
|
|
plans = {k: [] for k in ('it', 'r', 'qe', 'commit', 'points', 'midpoints', 'gaps', 'certified')} if return_traj else None
|
2026-10-17 16:35:52 +00:00
|
|
|
|
n_start = int(round(0.5 / DT)); moved_start = None # R2 起步失败:0.5 s 内关节总行程 < 1°
|
2026-10-19 10:43:05 +00:00
|
|
|
|
k2s = int(round(2.0 / DT)); e2 = None
|
|
|
|
|
|
e0, _ = goal_err(kin, q0, T, R)
|
2026-10-17 16:35:52 +00:00
|
|
|
|
j1_acc = torch.zeros(B, device=device); j1_max = torch.zeros(B, device=device); j1_min = torch.zeros(B, device=device)
|
2026-10-17 18:57:48 +00:00
|
|
|
|
j1_first = torch.zeros(B, device=device) # 2026-10-18 第五轮(审查 N-3):J1 累计偏离首次超过 FIRST_DEG 时的方向(真正的"首次方向")
|
2026-10-17 15:38:00 +00:00
|
|
|
|
cold = True
|
2026-10-19 10:43:05 +00:00
|
|
|
|
ar = torch.arange(B, device=device)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
for it in range(nmax):
|
|
|
|
|
|
tnow = it * DT
|
2026-10-19 10:43:05 +00:00
|
|
|
|
if (queue.shape[1] <= 3) if replan == 'r3' else (queue.shape[1] == 0):
|
2026-10-17 15:38:00 +00:00
|
|
|
|
r = queue.shape[1]
|
2026-10-20 17:26:20 +08:00
|
|
|
|
pre = torch.zeros(B, 3, 6, device=device)
|
|
|
|
|
|
qe = q
|
|
|
|
|
|
if r:
|
|
|
|
|
|
prefix_points, _ = execution_points(kin, q, queue)
|
|
|
|
|
|
pre[:, :r] = prefix_points - torch.cat([q[:, None], prefix_points[:, :-1]], 1)
|
|
|
|
|
|
qe = prefix_points[:, -1]
|
2026-10-17 15:38:00 +00:00
|
|
|
|
e, oe = goal_err(kin, qe, T, R)
|
|
|
|
|
|
near = (e < 5) & ((R.abs().sum(-1) == 0) | (oe < 5))
|
2026-10-19 10:43:05 +00:00
|
|
|
|
subs, nsub, sub_prev, sub_t = sub_advance(kin, subs, nsub, sub_prev, sub_t, qe, tnow, sub_rule)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
sf = (nsub > 0).float()
|
|
|
|
|
|
feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=device), subs[:, 0], sf)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
if oracle is not None:
|
|
|
|
|
|
act = oracle.plan(it, K, g); sub = torch.zeros(B, 13, device=device)
|
|
|
|
|
|
else:
|
|
|
|
|
|
z = torch.randn(B * K, H, 6, device=device, generator=g) * z_scale
|
|
|
|
|
|
act, val, sub = model.sample_k(feat, K, z, steps=steps)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
if sub_refresh and not cold: # 子目标头作为反应式规划:没有待走途经点时,用当前状态的预测补一个(训练标签在每个状态都有 sub)
|
|
|
|
|
|
need = sub[:, 12] > 0.5
|
|
|
|
|
|
newsubs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
|
|
|
|
|
|
rf = need & (nsub <= 0)
|
|
|
|
|
|
subs = torch.where(rf[:, None, None], newsubs, subs)
|
|
|
|
|
|
nsub = torch.where(rf, torch.full_like(nsub, 2), nsub)
|
|
|
|
|
|
sub_prev = torch.where(rf[:, None], q, sub_prev)
|
|
|
|
|
|
sub_t = torch.where(rf, torch.full_like(sub_t, tnow), sub_t)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
inc = act.float().clamp(-1, 1) * astep
|
2026-10-20 02:40:12 +08:00
|
|
|
|
if fb_gain > 0: # 实验(2026-10-20):目标反馈加在所有候选上(安全层也评估带反馈的候选)
|
2026-10-20 14:23:59 +08:00
|
|
|
|
if fb_cache is None or fb_every <= 1 or pit % fb_every == 0:
|
|
|
|
|
|
fb = dls_steps(kin, qe, T, R, n=1); fb_cache = fb
|
|
|
|
|
|
else:
|
|
|
|
|
|
fb = fb_cache
|
|
|
|
|
|
pit += 1
|
2026-10-20 17:06:53 +08:00
|
|
|
|
gm_ = None
|
|
|
|
|
|
if fb_clear > 0: # 近障碍衰减:反馈方向可能把候选推进障碍,贴近时让模型自己的绕行占主导
|
|
|
|
|
|
dgc = kin.gaps(qe, fb_clear)['gm'].min(-1).values
|
|
|
|
|
|
gm_ = torch.clamp(dgc / fb_clear, fb_cmin, 1.0)
|
2026-10-20 13:33:23 +08:00
|
|
|
|
if fb_ki > 0:
|
|
|
|
|
|
fb_i = fb_decay * fb_i + fb[:, 0] * (~done)[:, None]
|
|
|
|
|
|
lim = step * fb_imax
|
|
|
|
|
|
fb_i = fb_i.clamp(-lim, lim)
|
|
|
|
|
|
inc = inc + fb_gain * fb[:, :, None, :] + fb_ki * fb_i[:, None, None, :]
|
|
|
|
|
|
elif fb_scale > 0: # 距离衰减:远离目标全增益,贴近目标减弱(减少过冲/刹停)
|
2026-10-20 03:09:21 +08:00
|
|
|
|
dg, _ = goal_err(kin, qe, T, R)
|
|
|
|
|
|
gdec = fb_gain * torch.clamp(dg / fb_scale, fb_near, 1.0)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
if gm_ is not None: gdec = gdec * gm_
|
2026-10-20 03:09:21 +08:00
|
|
|
|
inc = inc + gdec[:, None, None, None] * fb[:, :, None, :]
|
|
|
|
|
|
else:
|
2026-10-20 17:06:53 +08:00
|
|
|
|
gv = torch.full((B,), fb_gain, device=device, dtype=fb.dtype)
|
|
|
|
|
|
if gm_ is not None: gv = gv * gm_
|
|
|
|
|
|
if fb_block_gain != 1.0: # 上一次被安全层全拒:减弱目标反馈,让模型自己的绕行候选有机会通过认证
|
|
|
|
|
|
gv = torch.where(last_brake, gv * fb_block_gain, gv)
|
|
|
|
|
|
inc = inc + gv[:, None, None, None] * fb[:, :, None, :]
|
2026-10-20 19:48:23 +08:00
|
|
|
|
if inc_cap:
|
|
|
|
|
|
inc = torch.maximum(torch.minimum(inc, step), -step) # 反馈之后才限幅:限的是真正要执行的每轴增量
|
2026-10-17 15:38:00 +00:00
|
|
|
|
if cold:
|
2026-10-19 10:43:05 +00:00
|
|
|
|
need = sub[:, 12] > 0.5
|
|
|
|
|
|
subs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
|
|
|
|
|
|
if sub_override is not None:
|
|
|
|
|
|
subs = sub_override[0].to(device).float()
|
|
|
|
|
|
n_ov = sub_override[1].to(device)
|
|
|
|
|
|
nsub_ov = n_ov.long() if n_ov.dtype != torch.bool else n_ov.long() * subs.shape[1]
|
|
|
|
|
|
need = nsub_ov > 0
|
|
|
|
|
|
nsub = nsub_ov
|
|
|
|
|
|
else:
|
|
|
|
|
|
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub))
|
|
|
|
|
|
if not use_sub: nsub = torch.zeros_like(nsub)
|
|
|
|
|
|
sub_t.fill_(tnow); sub_prev = q.clone()
|
2026-10-17 15:38:00 +00:00
|
|
|
|
fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6]
|
2026-10-19 03:42:46 +00:00
|
|
|
|
if select == 'first':
|
|
|
|
|
|
score = torch.zeros(B, K, device=device); score[:, 0] = 1
|
2026-10-20 22:24:49 +08:00
|
|
|
|
elif select in ('endpoint', 'adapt', 'ep_len', 'ep_waste'):
|
2026-10-20 15:48:31 +08:00
|
|
|
|
qa = fut[:, :, -1].reshape(B * K, 6)
|
|
|
|
|
|
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
|
|
|
|
|
|
score = -e_end.view(B, K)
|
2026-10-20 22:24:49 +08:00
|
|
|
|
if select == 'ep_len' and len_w > 0: # 行程惩罚:每轴按 STEP 归一的累计路程。§9.2 要“行程≤规划器×1.10”,
|
2026-10-20 22:05:12 +08:00
|
|
|
|
ln = ((fut[:, :, 1:] - fut[:, :, :-1]).abs() / step).sum(-1).sum(-1) # 而现在 2.4 倍没人管
|
|
|
|
|
|
score = score - len_w * ln
|
2026-10-20 22:24:49 +08:00
|
|
|
|
if select == 'ep_waste' and len_w > 0: # 轮次 39 的分解证明行程超出主要是同一轴来回抖(换向步占 23.5%、
|
|
|
|
|
|
dv = fut[:, :, 1:] - fut[:, :, :-1] # 净位移只占总行程 44.5%),绕远只占一小部分 ⇒ 罚"白费的比例":
|
|
|
|
|
|
tot = dv.abs().sum(-2).sum(-1) # 同轴来回会在净位移里相互抵消,所以 waste = 1 − 净/总 ∈ [0,1]。
|
|
|
|
|
|
net = dv.sum(-2).abs().sum(-1) # dv 是 [B,K,H-1,6]:时间在第 -2 轴(写错成 -1 就变成"对轴求和",等于总行程)
|
|
|
|
|
|
frac = (1.0 - net / tot.clamp_min(1e-6)).clamp(0, 1)
|
|
|
|
|
|
score = score - len_w * frac * e.view(B, 1) # 罚度按"当前离目标多远"缩放:近端不会把进度项压死
|
2026-10-20 17:06:53 +08:00
|
|
|
|
if select == 'adapt' and last is not None:
|
|
|
|
|
|
n = min(fut.shape[2], last.shape[1] - (commit_n + 1))
|
|
|
|
|
|
w = 0.85 ** torch.arange(n, device=device, dtype=torch.float32)
|
|
|
|
|
|
d2 = (((fut[:, :, :n] - last[:, None, commit_n + 1:commit_n + 1 + n]) / step) ** 2).sum(-1)
|
|
|
|
|
|
sc = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0]))
|
|
|
|
|
|
score = torch.where(last_brake[:, None], sc, score)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
elif cold or select == 'value':
|
2026-10-17 15:38:00 +00:00
|
|
|
|
# 短前瞻:候选末态的价值
|
|
|
|
|
|
qa = fut[:, :, -1].reshape(B * K, 6); qb = fut[:, :, -2].reshape(B * K, 6)
|
|
|
|
|
|
f2 = features(kin, qa, qb, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0), torch.zeros(B * K, 3, 6, device=device),
|
|
|
|
|
|
torch.zeros(B * K, device=device), qa, torch.zeros(B * K, device=device))
|
|
|
|
|
|
v2, _ = model.heads(model.encode(f2))
|
|
|
|
|
|
score = -v2.view(B, K)
|
|
|
|
|
|
else:
|
2026-10-19 03:42:46 +00:00
|
|
|
|
# 2026-10-19 修错位:last[i] = 上一计划从 qe_old 走 i 步后的位置;提交 3 步后 qe_new = last[3],
|
2026-10-19 10:43:05 +00:00
|
|
|
|
# 新候选 fut[0] 是 qe_new 再走 1 步 = 对应 last[4]。JS 部署(ik-policy.js)同一处同样修正。
|
2026-10-20 12:20:31 +08:00
|
|
|
|
n = min(fut.shape[2], last.shape[1] - (commit_n + 1))
|
2026-10-17 15:38:00 +00:00
|
|
|
|
w = 0.85 ** torch.arange(n, device=device, dtype=torch.float32)
|
2026-10-20 06:26:31 +08:00
|
|
|
|
d2 = (((fut[:, :, :n] - last[:, None, commit_n + 1:commit_n + 1 + n]) / step) ** 2).sum(-1)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
# 2026-10-19(审查 review_closedloop F5):上一次是刹停或 DLS 接管的行没有「已提交计划」,
|
|
|
|
|
|
# 与 JS(lastPlan = null → 一致性项缺席、各候选同分 → 取第 0 条)一致:同分,稳定排序取第 0 条
|
|
|
|
|
|
score = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0]))
|
2026-10-20 15:48:31 +08:00
|
|
|
|
if select == 'mix': # 一致性 + 端点进度(w=sel_w/mm)
|
|
|
|
|
|
qa = fut[:, :, -1].reshape(B * K, 6)
|
|
|
|
|
|
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
|
|
|
|
|
|
score = score - sel_w * e_end.view(B, K)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
order = torch.sort(score, dim=-1, descending=True, stable=True).indices
|
2026-10-17 15:38:00 +00:00
|
|
|
|
pick = order[:, 0]
|
2026-10-20 12:49:37 +08:00
|
|
|
|
if select == 'average': # 2026-10-20:候选平均(平滑误差)——所有候选置为 K 条动作的均值
|
|
|
|
|
|
inc = inc.mean(1, keepdim=True).expand_as(inc).contiguous()
|
|
|
|
|
|
K_eff = 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
K_eff = K
|
2026-10-20 06:26:31 +08:00
|
|
|
|
commit = inc[ar, pick, r:r + commit_n]
|
2026-10-20 08:42:29 +08:00
|
|
|
|
if collect is not None: # 反馈蒸馏:收集与部署完全一致的 (特征, 复合动作=模型输出+目标反馈)
|
|
|
|
|
|
collect.append((feat.detach().float().cpu(), inc[ar, pick].detach().float().cpu()))
|
2026-10-19 10:43:05 +00:00
|
|
|
|
committed = torch.ones(B, dtype=torch.bool, device=device)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
braked = torch.zeros(B, dtype=torch.bool, device=device)
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj:
|
|
|
|
|
|
cert_pts = torch.full((B, commit_n, 6), float('nan'), device=device)
|
|
|
|
|
|
cert_mids = cert_pts.clone()
|
|
|
|
|
|
cert_gaps = torch.full((B, 2 * commit_n), float('nan'), device=device)
|
|
|
|
|
|
certified = torch.zeros(B, commit_n, dtype=torch.bool, device=device)
|
2026-10-20 06:26:31 +08:00
|
|
|
|
if safety and safety_partial: # 2026-10-20:允许「部分安全前缀」——执行到第一个不安全步为止,而不是全拒刹停
|
|
|
|
|
|
maxlen = torch.zeros(B, dtype=torch.long, device=device)
|
|
|
|
|
|
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
|
2026-10-20 15:48:31 +08:00
|
|
|
|
ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff
|
2026-10-20 20:53:43 +08:00
|
|
|
|
if cert_batch: # 一次 gaps 覆盖全部候选:典型路径略贵,但把「前面候选被拒→串行调用 K 次」的最坏情况封成 1 次
|
2026-10-20 21:25:47 +08:00
|
|
|
|
cks = inc[ar[:, None], order[:, :ntry], r:r + commit_n] # [B,ntry,cn,6]
|
|
|
|
|
|
if backoff: # 缩步档与正常候选并进同一次认证,不额外调用几何
|
|
|
|
|
|
sc = (0.5 ** torch.arange(1, backoff + 1, device=device, dtype=inc.dtype))[None, :, None, None]
|
|
|
|
|
|
cands = torch.cat([cks, inc[ar, order[:, 0], r:r + commit_n][:, None] * sc], 1)
|
|
|
|
|
|
else:
|
|
|
|
|
|
cands = cks
|
|
|
|
|
|
nc = cands.shape[1]
|
|
|
|
|
|
pts, mids = execution_points(kin, qe.repeat_interleave(nc, 0), cands.reshape(B * nc, commit_n, 6))
|
|
|
|
|
|
gaps6 = kin.gaps(torch.cat([pts, mids], 1).reshape(-1, 6), gcap)['gm'].min(-1).values.view(B, nc, 2 * commit_n)
|
|
|
|
|
|
stepok = (gaps6 > gap_margin)[:, :, :commit_n] & (gaps6 > gap_margin)[:, :, commit_n:]
|
|
|
|
|
|
pl_all = torch.cumprod(stepok.long(), -1).sum(-1) # [B,nc] 每条候选的安全前缀长度
|
|
|
|
|
|
plen = pl_all[:, :ntry]
|
2026-10-20 20:53:43 +08:00
|
|
|
|
maxlen = plen.max(-1).values
|
|
|
|
|
|
best = plen.argmax(-1) # 与串行同口径:严格更大者胜 ⇒ 首个最大值
|
2026-10-20 21:25:47 +08:00
|
|
|
|
if backoff: # 缩步候选只在全拒时顶上:不参与「前缀更长者优先」的比较,所以正常路径逐位不变
|
|
|
|
|
|
full0 = maxlen == 0
|
|
|
|
|
|
bb = pl_all[:, ntry:].argmax(-1) # 平手取缩得最少的(档 0 = 1/2)
|
|
|
|
|
|
mv = pl_all[ar, ntry + bb]
|
|
|
|
|
|
bo += (full0 & (mv > 0))
|
|
|
|
|
|
best = torch.where(full0, ntry + bb, best)
|
|
|
|
|
|
maxlen = torch.where(full0, mv, maxlen)
|
|
|
|
|
|
chosen_k = torch.where(full0, order[:, 0], order[ar, best.clamp(max=ntry - 1)])
|
|
|
|
|
|
else:
|
|
|
|
|
|
chosen_k = order[ar, best]
|
|
|
|
|
|
chosen = cands[ar, best]
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj:
|
2026-10-20 21:25:47 +08:00
|
|
|
|
cert_pts = pts.view(B, nc, commit_n, 6)[ar, best]
|
|
|
|
|
|
cert_mids = mids.view(B, nc, commit_n, 6)[ar, best]
|
2026-10-20 20:53:43 +08:00
|
|
|
|
cert_gaps = gaps6[ar, best]
|
|
|
|
|
|
else:
|
|
|
|
|
|
for kk in range(ntry):
|
|
|
|
|
|
ck = inc[ar, order[:, kk], r:r + commit_n]
|
|
|
|
|
|
pts, mids = execution_points(kin, qe, ck)
|
|
|
|
|
|
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
|
|
|
|
|
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
|
|
|
|
|
ok6 = gaps6 > gap_margin
|
|
|
|
|
|
okstep = ok6[:, :commit_n] & ok6[:, commit_n:]
|
|
|
|
|
|
plen = torch.cumprod(okstep.long(), 1).sum(1)
|
|
|
|
|
|
take = plen > maxlen
|
|
|
|
|
|
chosen = torch.where(take[:, None, None], ck, chosen); chosen_k = torch.where(take, order[:, kk], chosen_k)
|
|
|
|
|
|
if return_traj:
|
|
|
|
|
|
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
|
|
|
|
|
|
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
|
|
|
|
|
|
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
|
|
|
|
|
|
maxlen = torch.maximum(maxlen, plen)
|
|
|
|
|
|
if B == 1 and int(maxlen[0]) >= commit_n: break # 实时单题:首个全安全候选即停(安全层 16 点→2-4 点)
|
2026-10-20 06:26:31 +08:00
|
|
|
|
msk = (torch.arange(commit_n, device=device)[None] < maxlen[:, None])
|
|
|
|
|
|
commit = torch.where(msk[..., None], chosen, torch.zeros_like(chosen))
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj: certified = msk.clone()
|
2026-10-20 17:06:53 +08:00
|
|
|
|
brakes += (maxlen == 0).float(); braked = maxlen == 0
|
2026-10-20 06:26:31 +08:00
|
|
|
|
pick = chosen_k; committed = maxlen > 0
|
|
|
|
|
|
elif safety:
|
|
|
|
|
|
ok_any = torch.zeros(B, dtype=torch.bool, device=device)
|
|
|
|
|
|
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
|
2026-10-20 15:48:31 +08:00
|
|
|
|
ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff
|
|
|
|
|
|
for kk in range(ntry):
|
2026-10-20 06:26:31 +08:00
|
|
|
|
ck = inc[ar, order[:, kk], r:r + commit_n]
|
2026-10-20 17:26:20 +08:00
|
|
|
|
pts, mids = execution_points(kin, qe, ck)
|
2026-10-20 06:26:31 +08:00
|
|
|
|
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
2026-10-20 19:48:23 +08:00
|
|
|
|
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
|
|
|
|
|
okk = (gaps6 > gap_margin).all(-1)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
take = okk & ~ok_any
|
2026-10-19 10:43:05 +00:00
|
|
|
|
chosen = torch.where(take[:, None, None], ck, chosen); chosen_k = torch.where(take, order[:, kk], chosen_k); ok_any |= okk
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj:
|
|
|
|
|
|
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
|
|
|
|
|
|
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
|
|
|
|
|
|
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
commit = torch.where(ok_any[:, None, None], chosen, torch.zeros_like(chosen))
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj: certified = ok_any[:, None].expand(-1, commit_n).clone()
|
2026-10-20 17:06:53 +08:00
|
|
|
|
brakes += (~ok_any).float(); braked = ~ok_any
|
2026-10-19 10:43:05 +00:00
|
|
|
|
pick = chosen_k; committed = ok_any
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if safety and fb_gain > 0 and fb_fallback > 0:
|
|
|
|
|
|
fbcom = torch.zeros(B, commit_n, 6, device=device)
|
|
|
|
|
|
fbcom[:, :1] = dls_steps(kin, qe, T, R, n=1) * fb_fallback
|
|
|
|
|
|
pts, mids = execution_points(kin, qe, fbcom)
|
|
|
|
|
|
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
2026-10-20 19:48:23 +08:00
|
|
|
|
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
|
|
|
|
|
takef = braked & (gaps6 > gap_margin).all(-1)
|
2026-10-20 17:26:20 +08:00
|
|
|
|
commit = torch.where(takef[:, None, None], fbcom, commit)
|
|
|
|
|
|
if return_traj:
|
|
|
|
|
|
cert_pts = torch.where(takef[:, None, None], pts, cert_pts)
|
|
|
|
|
|
cert_mids = torch.where(takef[:, None, None], mids, cert_mids)
|
|
|
|
|
|
cert_gaps = torch.where(takef[:, None], gaps6, cert_gaps)
|
|
|
|
|
|
certified = certified | takef[:, None]
|
2026-10-17 15:38:00 +00:00
|
|
|
|
if near.any():
|
2026-10-20 06:26:31 +08:00
|
|
|
|
ref = dls_steps(kin, qe, T, R, n=commit_n)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
take = near
|
|
|
|
|
|
if dls_safety and safety: # 2026-10-20:近目标 DLS 接管也要过安全层(部署端 _certCommit 本来就会验;不过则退回候选计划)
|
2026-10-20 17:26:20 +08:00
|
|
|
|
pts, mids = execution_points(kin, qe, ref)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
|
2026-10-20 19:48:23 +08:00
|
|
|
|
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
|
|
|
|
|
|
okd = (gaps6 > gap_margin).all(-1)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
take = near & okd
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj:
|
|
|
|
|
|
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
|
|
|
|
|
|
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
|
|
|
|
|
|
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
|
|
|
|
|
|
if return_traj:
|
|
|
|
|
|
certified = torch.where(take[:, None], torch.full_like(certified, bool(dls_safety and safety)), certified)
|
2026-10-20 17:06:53 +08:00
|
|
|
|
commit = torch.where(take[:, None, None], ref, commit)
|
|
|
|
|
|
dls_it = torch.where(take & (dls_it < 0), torch.full_like(dls_it, it), dls_it)
|
|
|
|
|
|
committed = committed & ~take
|
2026-10-19 10:43:05 +00:00
|
|
|
|
# 2026-10-19(F5):last 记真正提交的那条候选(安全层换了候选时是换后的那条);刹停 / DLS 接管的行无效
|
2026-10-20 13:33:23 +08:00
|
|
|
|
if fb_ki > 0: # 刹停/精修接管时清积分(防止原地累积到限幅后突然冲出去)
|
|
|
|
|
|
fb_i = torch.where(committed[:, None], fb_i, torch.zeros_like(fb_i))
|
2026-10-20 17:06:53 +08:00
|
|
|
|
last = torch.cat([qe[:, None], fut[ar, pick]], 1); last_ok = committed; last_brake = braked
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj:
|
|
|
|
|
|
plans['it'].append(it + r)
|
2026-10-20 21:16:27 +08:00
|
|
|
|
plans['r'].append(r)
|
2026-10-20 17:26:20 +08:00
|
|
|
|
for key, value in (('qe', qe), ('commit', commit), ('points', cert_pts), ('midpoints', cert_mids), ('gaps', cert_gaps), ('certified', certified)):
|
|
|
|
|
|
plans[key].append(value)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
queue = torch.cat([queue, commit], 1)
|
|
|
|
|
|
cold = False
|
|
|
|
|
|
d = queue[:, 0]; queue = queue[:, 1:]
|
|
|
|
|
|
d = torch.where(done[:, None], torch.zeros_like(d), d)
|
|
|
|
|
|
qp = q; q = kin.clamp_box(q + d)
|
|
|
|
|
|
travel += d.abs().sum(-1)
|
2026-10-17 16:35:52 +00:00
|
|
|
|
j1_acc = j1_acc + d[:, 0]; j1_max = torch.maximum(j1_max, j1_acc); j1_min = torch.minimum(j1_min, j1_acc)
|
2026-10-17 18:57:48 +00:00
|
|
|
|
j1_first = torch.where((j1_first == 0) & (j1_acc.abs() >= FIRST_DEG), torch.sign(j1_acc), j1_first)
|
2026-10-17 16:35:52 +00:00
|
|
|
|
if it + 1 == n_start: moved_start = travel.clone()
|
2026-10-17 15:38:00 +00:00
|
|
|
|
if return_traj: traj.append(q.clone())
|
|
|
|
|
|
mid = qp + 0.5 * (q - qp)
|
|
|
|
|
|
gm = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
|
2026-10-19 10:43:05 +00:00
|
|
|
|
tn = (it + 1) * DT
|
|
|
|
|
|
# 2026-10-19(审查 review_closedloop F4):只计每题自己时限内的碰撞(同批长题会把短题多仿真几秒)
|
|
|
|
|
|
hit = ((gm[:B] <= 0) | (gm[B:] <= 0)) & ~done & (tn <= limit + 1e-6)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
col += hit.float()
|
2026-10-19 10:43:05 +00:00
|
|
|
|
first_hit = torch.where(hit & torch.isnan(first_hit), torch.full_like(first_hit, tn), first_hit)
|
2026-10-20 19:48:23 +08:00
|
|
|
|
act_ = ((~done) & (tn <= limit + 1e-6)).float() # 只计自己时限内的移动(与 col 同口径)
|
|
|
|
|
|
exec_d = (q - qp).abs()
|
|
|
|
|
|
vratio = torch.maximum(vratio, (exec_d / step).max(-1).values * act_) # 实际速度 / VMAX
|
|
|
|
|
|
aratio = torch.maximum(aratio, ((exec_d - prev_exec) / step).max(-1).values * act_) # 每控制步的速度变化 / VMAX
|
|
|
|
|
|
prev_exec = exec_d
|
2026-10-20 22:26:34 +08:00
|
|
|
|
sgn = torch.sign(q - qp) # 换向率:轮次 39 的分解证明"行程超出 2.4 倍"主要是同一轴来回抖,不是绕远
|
|
|
|
|
|
rev += ((sgn != 0) & (prev_sgn != 0) & (sgn == -prev_sgn)).float().sum(-1) * act_
|
|
|
|
|
|
prev_sgn = torch.where(sgn != 0, sgn, prev_sgn) # 零步不重置方向(站住再走同向不算换向)
|
|
|
|
|
|
nact += act_
|
2026-10-17 15:38:00 +00:00
|
|
|
|
e, oe = goal_err(kin, q, T, R)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
if it + 1 == k2s: e2 = e.clone()
|
2026-10-17 15:38:00 +00:00
|
|
|
|
newly = (~done) & (e < 1) & (oe < 0.5)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
t_done = torch.where(newly, torch.full_like(t_done, tn), t_done); done |= newly
|
2026-10-20 17:06:53 +08:00
|
|
|
|
if progress is not None and (it == 0 or (it + 1) % 50 == 0 or it + 1 == nmax):
|
|
|
|
|
|
progress(it + 1, nmax)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
if done.all(): break
|
2026-10-19 10:43:05 +00:00
|
|
|
|
ok = done & (t_done <= limit + 1e-6)
|
2026-10-17 16:35:52 +00:00
|
|
|
|
if moved_start is None: moved_start = travel.clone()
|
|
|
|
|
|
e_end, _ = goal_err(kin, q, T, R)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
if e2 is None: e2 = e_end
|
|
|
|
|
|
ratio2 = e2 / e0.clamp(min=1e-3)
|
2026-10-17 16:35:52 +00:00
|
|
|
|
start_fail = (moved_start < 1.0) & ~(done & (t_done <= 0.5))
|
|
|
|
|
|
okr = ok & has_ref & torch.isfinite(tr)
|
|
|
|
|
|
res = {'n': B, 'n_ref': int(has_ref.sum()), 'success': ok.float().mean().item(),
|
|
|
|
|
|
'time_ok_1p3': (ok & has_ref & (t_done <= 1.3 * ex)).float().sum().item() / max(1, int(has_ref.sum())) if has_ref.any() else None,
|
2026-10-17 15:38:00 +00:00
|
|
|
|
'collision_rate': (col > 0).float().mean().item(), 'collision_steps_mean': col.mean().item(),
|
2026-10-17 16:35:52 +00:00
|
|
|
|
'travel_ratio_median': (travel[okr] / tr[okr].clamp(min=1)).median().item() if okr.any() else None,
|
|
|
|
|
|
'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item(),
|
2026-10-20 21:25:47 +08:00
|
|
|
|
'backoff_mean': bo.mean().item(),
|
2026-10-19 10:43:05 +00:00
|
|
|
|
'start_fail': start_fail.float().mean().item(), 'goal_ratio_2s': ratio2.median().item(),
|
2026-10-20 19:48:23 +08:00
|
|
|
|
'speed_ratio_mean': vratio.mean().item(), 'over_vmax_rate': (vratio > 1.001).float().mean().item(),
|
2026-10-17 16:35:52 +00:00
|
|
|
|
'time_budget': 'teacher×2(≥3 s)' if bool(has_ref.all()) else ('fixed %.0f s' % (max_sec or NOREF_SEC) if not has_ref.any() else 'mixed')}
|
|
|
|
|
|
# 绕行侧:J1 累计位移的正向/负向极值哪个更大(+1 / −1),供 R7 左右比例门禁
|
2026-10-17 18:57:48 +00:00
|
|
|
|
# 首次方向;全程没偏离到 FIRST_DEG 的(基本不绕 J1)退回净方向
|
|
|
|
|
|
side = torch.where(j1_first != 0, j1_first, torch.where(j1_max >= -j1_min, torch.ones_like(j1_max), -torch.ones_like(j1_max)))
|
2026-10-19 10:43:05 +00:00
|
|
|
|
per = {'done': done.cpu(), 'ok': ok.cpu(), 't_done': t_done.cpu(), 'limit': limit.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
|
2026-10-20 21:25:47 +08:00
|
|
|
|
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu(), 'backoff': bo.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(),
|
2026-10-20 22:26:34 +08:00
|
|
|
|
'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu(),
|
2026-10-20 22:28:10 +08:00
|
|
|
|
# 口径与离线分解一致:换向的 (步,轴) 对数 / 活动的 (步,轴) 对数 ∈ [0,1](除以 6 个轴)
|
|
|
|
|
|
'reverse_rate': (rev / (6.0 * nact.clamp_min(1.0))).cpu()}
|
2026-10-20 17:26:20 +08:00
|
|
|
|
if return_traj:
|
|
|
|
|
|
per['traj'] = torch.stack(traj, 1).cpu()
|
2026-10-20 21:16:27 +08:00
|
|
|
|
per['plans'] = {k: torch.tensor(v) if k in ('it', 'r') else torch.stack(v, 1).cpu() for k, v in plans.items()}
|
2026-10-17 15:38:00 +00:00
|
|
|
|
return res, per
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def evaluate(model, kin, tasks, device, K=8, safety=False, bs=256, seed=0):
|
2026-10-17 16:35:52 +00:00
|
|
|
|
if not tasks: # 小样本摸底时 val 可能为空
|
2026-10-19 10:43:05 +00:00
|
|
|
|
return {'n': 0, 'success': float('nan'), 'collision_rate': float('nan'), 'start_fail': float('nan'), 'brakes_mean': float('nan'),
|
|
|
|
|
|
'goal_ratio_2s': float('nan')}, []
|
2026-10-17 15:38:00 +00:00
|
|
|
|
agg, pers = {}, []
|
|
|
|
|
|
for i in range(0, len(tasks), bs):
|
|
|
|
|
|
r, p = rollout(model, kin, tasks[i:i + bs], device, K=K, safety=safety, seed=seed + i)
|
|
|
|
|
|
w = r['n']; pers.append((r, p))
|
|
|
|
|
|
for k, v in r.items():
|
2026-10-17 16:35:52 +00:00
|
|
|
|
if k == 'n' or v is None or isinstance(v, str): continue
|
2026-10-17 15:38:00 +00:00
|
|
|
|
agg[k] = agg.get(k, 0.0) + v * w
|
|
|
|
|
|
n = sum(r['n'] for r, _ in pers)
|
|
|
|
|
|
out = {k: v / n for k, v in agg.items()}; out['n'] = n
|
2026-10-17 16:35:52 +00:00
|
|
|
|
out['n_ref'] = sum(r['n_ref'] for r, _ in pers)
|
2026-10-19 10:43:05 +00:00
|
|
|
|
# 中位数不能按批加权平均:全体任务重算(2026-10-19,选模分数用它)
|
|
|
|
|
|
out['goal_ratio_2s'] = torch.cat([p['goal_ratio_2s'] for _, p in pers]).median().item()
|
2026-10-17 16:35:52 +00:00
|
|
|
|
out['time_budget'] = pers[0][0]['time_budget'] if len({r['time_budget'] for r, _ in pers}) == 1 else 'mixed'
|
2026-10-17 15:38:00 +00:00
|
|
|
|
return out, pers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2026-10-19 10:43:05 +00:00
|
|
|
|
from ml.common.ckpt import load_policy
|
2026-10-17 15:38:00 +00:00
|
|
|
|
from ml.data import load_tasks
|
|
|
|
|
|
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=1000)
|
|
|
|
|
|
ap.add_argument('--safety', action='store_true'); 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()
|
2026-10-19 10:43:05 +00:00
|
|
|
|
model, ck = load_policy(a.ckpt, a.device)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
kin = Kin(device=a.device)
|
|
|
|
|
|
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
|
|
|
|
|
|
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]
|
|
|
|
|
|
res, pers = evaluate(model, kin, tasks, a.device, K=a.K, safety=a.safety)
|
|
|
|
|
|
by = {}
|
2026-10-19 10:43:05 +00:00
|
|
|
|
flat_ok = torch.cat([p['ok'] for _, p in pers]); flat_col = torch.cat([p['col'] for _, p in pers])
|
|
|
|
|
|
for t, d, c in zip(tasks, flat_ok.tolist(), flat_col.tolist()):
|
2026-10-17 15:38:00 +00:00
|
|
|
|
k = t.get('cat', '?') + '/' + t.get('mode', '?'); b = by.setdefault(k, [0, 0, 0]); b[0] += 1; b[1] += int(d); b[2] += int(c > 0)
|
|
|
|
|
|
res['by_cat_mode'] = {k: {'n': v[0], 'success': v[1] / v[0], 'collision': v[2] / v[0]} for k, v in by.items()}
|
|
|
|
|
|
s = json.dumps(res, indent=1); print(s)
|
2026-10-17 18:57:48 +00:00
|
|
|
|
if a.out: # 先写 .tmp 再改名:中断不会留下半截结果被当成已完成(M5 续跑按文件存在跳过)
|
|
|
|
|
|
open(a.out + '.tmp', 'w').write(s); os.replace(a.out + '.tmp', a.out)
|
2026-10-17 15:38:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
main()
|