exp+feat: 轮次36(续)——时间账证明限幅后的损失是"站住"不是巡航;全拒缩步顶上(backoff,默认关)

This commit is contained in:
cbb
2026-10-20 21:25:47 +08:00
parent f23f6fc93f
commit 349814db7e
5 changed files with 224 additions and 12 deletions
+68
View File
@@ -713,5 +713,73 @@ class CertBatchEquivalenceTest(unittest.TestCase):
self.assertEqual(kin.gm_groups(q5, [(0, 2)], 40.0, xf=xf5).shape, (5, 1))
class BackoffTest(unittest.TestCase):
"""#17:全拒时把首选候选缩步顶上——不许改变「还有安全前缀」的题,但要把站住变成慢爬。"""
LIM = 0.5 # 桩里 J1 超过它就是碰撞(一步满速 = VMAX 60°/s × DT 0.02 = 1.2°)
class StubModel:
act_scale = 1.0
def __init__(self, act):
self.act = act
def sample_k(self, feat, K, z, steps=1):
B = feat.shape[0]
return (self.act.expand(B, K, 16, 6).clone(), torch.zeros(B, device=feat.device),
torch.zeros(B, 13, device=feat.device))
def run1(self, acts, backoff, cert_batch=1):
class StepKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
bad = q[:, 0] > BackoffTest.LIM
return {'gm': torch.where(bad[:, None], -torch.ones(len(q), 49, device=q.device),
torch.full((len(q), 49), 1.0, device=q.device))}
model = self.StubModel(torch.tensor(acts, dtype=torch.float32))
tasks = [{'q0': [0] * 6, 'T': [10, 0, 0]}]
with patch('ml.sim.closed_loop.features', return_value=torch.zeros(1, 83)), \
patch('ml.sim.closed_loop.goal_err',
side_effect=lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0]))):
res, per = rollout(model, StepKin(), tasks, 'cpu', K=len(acts), safety=True, max_sec=0.06,
return_traj=True, use_sub=False, select='first', replan='empty', commit_n=2,
safety_partial=1, cert_batch=cert_batch, backoff=backoff)
return res, per
def test_full_rejection_creeps_instead_of_standing_still(self):
acts = [[[1.0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
_, p0 = self.run1(acts, 0)
self.assertEqual(p0['traj'][0, :, 0].tolist(), [0.0] * 4) # 0.06 s 内规划 2 次,两次都全拒
self.assertEqual(float(p0['brakes'][0]), 2.0)
self.assertEqual(float(p0['backoff'][0]), 0.0)
_, p1 = self.run1(acts, 1) # 1/2 档 = 0.6° 仍然越界 ⇒ 与不开缩步逐位相同
torch.testing.assert_close(p1['traj'], p0['traj'], rtol=0, atol=0)
torch.testing.assert_close(p1['brakes'], p0['brakes'], rtol=0, atol=0)
_, p2 = self.run1(acts, 2) # 1/4 档 = 0.3° 安全 ⇒ 先爬一步,再规划时原地缩步也被拒
self.assertEqual([round(v, 5) for v in p2['traj'][0, :, 0].tolist()], [0.0, 0.3, 0.3, 0.3])
self.assertEqual(float(p2['backoff'][0]), 1.0)
self.assertEqual(float(p2['brakes'][0]), 1.0)
def test_backoff_never_overrides_a_candidate_with_a_safe_prefix(self):
# 第 1 步安全(0.24°)、第 2 步越界(0.84°)⇒ 前缀 1 步就够,缩步档不该插手
acts = [[[0.2, 0, 0, 0, 0, 0], [0.5, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
_, p0 = self.run1(acts, 0)
_, p2 = self.run1(acts, 2)
for key in ('traj', 'brakes', 'backoff'):
with self.subTest(key=key):
torch.testing.assert_close(p2[key], p0[key], rtol=0, atol=0)
for key in ('commit', 'certified', 'points', 'gaps'):
with self.subTest(key=key):
torch.testing.assert_close(p2['plans'][key], p0['plans'][key], rtol=0, atol=0)
self.assertGreater(float(p2['traj'][0, 1, 0]), 0.0)
def test_backoff_requires_batched_certification(self):
acts = [[[1.0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
with self.assertRaisesRegex(ValueError, 'cert_batch'):
self.run1(acts, 1, cert_batch=0)
if __name__ == '__main__':
unittest.main()