From b3c6ad995ad05ed559cc129abb69e1023ab2cd21 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 21 Oct 2026 00:58:37 +0800 Subject: [PATCH] =?UTF-8?q?exp+test:=20#30=20=E6=9C=BA=E5=88=B6=E8=90=BD?= =?UTF-8?q?=E5=BA=93=E2=80=94=E2=80=94select=3Dpre=20=E7=9A=84=E6=89=93?= =?UTF-8?q?=E5=88=86=E6=AD=A5=E9=95=BF=E5=81=9A=E6=88=90=E5=8F=82=E6=95=B0?= =?UTF-8?q?=20pre=5Fh=EF=BC=88h=3D4/8=20=E4=B8=A4=E8=87=82=E6=8E=92?= =?UTF-8?q?=E9=98=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pre_h=0 保持"0=commit_n"的轮次46 口径逐位不变;pre_h>0 按绝对步数取 fut 的该步终点误差。 新增两条可手算的期望值:夹具交叉点 h=4 选候选0(0.343 vs 0.20)、h=12 选候选1(0.114 vs 0.60)、 h=16 必须与 endpoint 同选;再加一条不变式 pre_h=0 与 pre_h=commit_n 的轨迹逐位相同。 51 项测试绿;ast 枚举确认新局部名没有遮蔽任何 rollout 形参(上一轮 nmax 那类事故的自查)。 判读规则已预注册,写在 ml/runs/_r48_horizon.sh 顶部。 --- ml/eval/eval_cl.py | 3 ++- ml/sim/closed_loop.py | 4 ++-- tests/test_eval_cl.py | 18 ++++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/ml/eval/eval_cl.py b/ml/eval/eval_cl.py index 8ff4498..1a7bb26 100644 --- a/ml/eval/eval_cl.py +++ b/ml/eval/eval_cl.py @@ -120,6 +120,7 @@ def main(): ap.add_argument('--z_scale', type=float, default=1.0, help='候选噪声缩放:>1 让 K 条候选更分散(测"全拒是没有备选"还是"真的没路")') ap.add_argument('--len_w', type=float, default=0.0, help="ep_len=罚累计归一行程;ep_waste=罚(1−净/总)×当前误差;0 与 endpoint 逐位相同") ap.add_argument('--tie', type=int, default=0, help='1=等长安全前缀时按沿路最小间隙定夺(需 --cert_batch 1)') + ap.add_argument('--pre_h', type=int, default=0, help="select=pre 的打分步长:0=commit_n(被执行前缀),>0=按绝对步数(#30 曲线)") ap.add_argument('--backoff', type=int, default=0, help='全拒时把首选候选按 1/2、1/4…缩步重试的档数(需 --cert_batch 1)') ap.add_argument('--max_sec', type=float, default=None) ap.add_argument('--seed', type=int, default=0) @@ -147,7 +148,7 @@ def main(): safety_partial=(a.commit if (a.safety and a.partial) else 0), commit_n=a.commit, select=a.select, replan=a.replan, fb_gain=a.fb, fb_scale=a.fb_scale, fb_near=a.fb_near, fb_fallback=a.fb_fallback, fb_ki=a.ki, fb_decay=a.kdecay, fb_imax=a.kimax, fb_every=a.fb_every, safety_try=a.safety_try, sel_w=a.sel_w, dls_safety=a.dls_safety, fb_clear=a.fb_clear, fb_cmin=a.fb_cmin, sub_refresh=a.sub_refresh, fb_block_gain=a.fb_block_gain, - inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, backoff=a.backoff, len_w=a.len_w, tie=a.tie, return_traj=bool(a.trace)) + inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, backoff=a.backoff, len_w=a.len_w, tie=a.tie, pre_h=a.pre_h, return_traj=bool(a.trace)) wall = time.perf_counter() - t0 result = {**meta, 'metrics': r, 'simulation_wall_s': wall} print(json.dumps(json_finite(result), ensure_ascii=False, allow_nan=False), flush=True) diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index 707405a..7c37453 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -163,7 +163,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross', fb_gain=0.0, fb_scale=0.0, fb_near=0.2, fb_fallback=0.0, safety_partial=0, commit_n=3, collect=None, 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, - inc_cap=0, gap_margin=0.0, cert_batch=0, backoff=0, len_w=0.0, tie=0, progress=None): + inc_cap=0, gap_margin=0.0, cert_batch=0, backoff=0, len_w=0.0, tie=0, pre_h=0, progress=None): """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 条)。 @@ -294,7 +294,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, score = torch.zeros(B, K, device=device); score[:, 0] = 1 elif select == 'pre': # #29:按「这一轮真会执行到的位置」打分,而不是 16 步开环外推的末端。 # 诊断来自 ep_len/ep_waste 的失败:打分地平线远长于提交步数,被安全层截断的候选那个终点根本到不了。 - qa_pre = fut[:, :, min(commit_n, fut.shape[2]) - 1].reshape(B * K, 6) + qa_pre = fut[:, :, min(pre_h if pre_h > 0 else commit_n, fut.shape[2]) - 1].reshape(B * K, 6) e_pre, _ = goal_err(kin, qa_pre, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0)) score = -e_pre.view(B, K) elif select in ('endpoint', 'adapt', 'ep_len', 'ep_waste'): diff --git a/tests/test_eval_cl.py b/tests/test_eval_cl.py index 110615d..54b9e97 100644 --- a/tests/test_eval_cl.py +++ b/tests/test_eval_cl.py @@ -987,7 +987,7 @@ class PreHorizonTest(unittest.TestCase): def gaps(self, q, cap): return {'gm': torch.ones(q.shape[0], 49)} - def run1(self, select, commit_n=2): + def run1(self, select, commit_n=2, pre_h=0): # 候选0:前 2 步各 +0.2(前缀 0.4),随后 14 步 −0.4/14 ⇒ 地平线末端回到 0 # 候选1:16 步各 +0.05 ⇒ 前缀只有 0.1,但地平线末端 0.8 c0 = [0.2, 0.2] + [-(0.4 / 14.0)] * 14 @@ -1000,7 +1000,7 @@ class PreHorizonTest(unittest.TestCase): patch('ml.sim.closed_loop.goal_err', side_effect=err): _, per = rollout(self.StubModel(acts), self.NearKin(), [{'q0': [0] * 6, 'T': [1, 0, 0]}], 'cpu', K=2, safety=False, max_sec=0.04, return_traj=True, use_sub=False, - select=select, replan='empty', commit_n=commit_n) + select=select, replan='empty', commit_n=commit_n, pre_h=pre_h) return per def test_pre_and_endpoint_pick_opposite(self): @@ -1010,6 +1010,20 @@ class PreHorizonTest(unittest.TestCase): self.assertAlmostEqual(float(p_pre['traj'][0, 1, 0]), 0.2 * 1.2, places=5) # 前缀 0.4 ⇒ 选候选0 self.assertNotAlmostEqual(float(p_end['traj'][0, 1, 0]), float(p_pre['traj'][0, 1, 0]), places=3) + def test_pre_h_moves_the_pick_back_at_the_crossover(self): + """#30 的曲线靠这个参数才量得动。期望值按夹具手算(单位=act 增量之和,STEP=1.2): + 候选0 累计 = 0.4 − max(0,h−2)·0.4/14,候选1 累计 = 0.05h ⇒ + h=4:0.3429 vs 0.20 ⇒ 选 0;h=12:0.1143 vs 0.60 ⇒ 选 1;h=16:0 vs 0.8 ⇒ 选 1。""" + for h, want in ((4, 0.2 * 1.2), (12, 0.05 * 1.2), (16, 0.05 * 1.2)): + p = self.run1('pre', pre_h=h) + self.assertAlmostEqual(float(p['traj'][0, 1, 0]), want, places=5, msg='h=%d' % h) + + def test_pre_h_zero_means_commit_n(self): + """默认(pre_h=0)必须与显式 pre_h=commit_n 逐位相同,否则"0=前缀"这句是空话。""" + a = self.run1('pre', commit_n=2, pre_h=0) + b = self.run1('pre', commit_n=2, pre_h=2) + torch.testing.assert_close(a['traj'], b['traj'], rtol=0, atol=0) + def test_pre_equals_endpoint_when_commit_is_the_whole_horizon(self): """不变式:commit_n=16 = 整条地平线 ⇒ pre 的索引落到末端,必须与 endpoint 选同一条。""" p = self.run1('pre', commit_n=16)