exp+fix: 轮次31——确定界连续认证抓到段内碰撞,但单段P50 22.9ms进不了20ms;自适应cap收益经交错复测撤回
- Kin.cert_lever 用三角不等式给全姿态位移界(替代 JS 随机采样×1.05),界量级 5.93–48.86 mm/整步 - Kin.certify_segments 批量前沿二分+每根查询预算:48 固定段中预算32判安全37/碰撞1/未知拒10, 第180题1.08–1.10s漏检段以5次查询判碰撞;未知只拒不放 - 修 seg_seg_dist 第二条退化成点不投影(模型内退化胶囊686受影响);但7586个加密点的组最小间隙 逐位不变、2000样本JS对拍仍 PASS → 不解释轮次30漏检、不改既有成绩 - 负结论:B=1 单段认证 P50 22.93/P90 58.43/P99 101.57 ms,432次中225次>20ms; 自适应宽相位 cap 交错复测耗时无收益,先前1.6×为GPU顺序漂移,已在日志撤回 - 成本模型 L=ceil(log2(bound/(ga+gb)))、查询=2^L+1 与实测对齐(安全段2–17次,未知段33–65次) - 新增8项段认证测试+1项回放测试(14→23项通过);变异实测证明张量 max_depth 回绕会误放行
This commit is contained in:
+137
-1
@@ -9,8 +9,9 @@ from unittest.mock import patch
|
||||
import torch
|
||||
|
||||
from ml.eval.eval_cl import json_finite, run
|
||||
from ml.eval.diag_collision import replay_dense, replay_first_hits
|
||||
from ml.eval.diag_collision import replay_certificates, replay_dense, replay_first_hits
|
||||
from ml.sim.closed_loop import TeacherOracle, rollout
|
||||
from ml.common.kin_torch import Kin, seg_seg_dist
|
||||
|
||||
|
||||
class EvalMetricsTest(unittest.TestCase):
|
||||
@@ -124,6 +125,30 @@ class EvalMetricsTest(unittest.TestCase):
|
||||
self.assertGreater(row['min_gap64_mm'], 0)
|
||||
self.assertEqual(row['steps'], 1)
|
||||
|
||||
def test_certificate_replay_samples_actual_movement_before_deadlines(self):
|
||||
kin = SegmentCertificateTest.ThinObstacle()
|
||||
kin.fp = 'probe'
|
||||
traj = torch.zeros(3, 4, 6)
|
||||
traj[:, :, 0] = torch.tensor([[0, 1, 1, 1], [0, -0.5, -1, -1], [0, 0, 1, 1]])
|
||||
per = {'traj': traj, 'limit': torch.tensor([0.06, 0.02, 0.02]),
|
||||
't_done': torch.tensor([float('nan'), 0.02, float('nan')]),
|
||||
'plans': {'it': torch.arange(3), 'commit': torch.zeros(3, 3, 1, 6),
|
||||
'certified': torch.ones(3, 3, 1, dtype=torch.bool), 'gaps': torch.full((3, 3, 2), 0.5)}}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
trace, output = Path(folder) / 'trace.pt', Path(folder) / 'audit.json'
|
||||
torch.save({'kin_fp': 'probe', 'tasks': [{}] * 3, 'batches': [({}, per, 3)]}, trace)
|
||||
with patch('ml.eval.diag_collision.Kin', return_value=kin), contextlib.redirect_stdout(io.StringIO()):
|
||||
result = replay_certificates(trace, [0, 1, 2], out=output, repeats=1, extra_segments=[(0, 0)])
|
||||
with self.assertRaisesRegex(ValueError, 'extra segment'):
|
||||
replay_certificates(trace, [1], repeats=1, extra_segments=[(1, 1)])
|
||||
self.assertEqual(json.loads(output.read_text())['dense_collision_segments'], 1)
|
||||
self.assertEqual([(r['i'], r['start_it']) for r in result['rows']], [(0, 0), (1, 0)])
|
||||
self.assertEqual(result['budgets']['32']['safe'], 1)
|
||||
self.assertEqual(result['budgets']['32']['collision'], 1)
|
||||
self.assertEqual(result['accepted_dense_collision_segments'], 0)
|
||||
self.assertTrue(result['batch_single_match'])
|
||||
self.assertEqual(result['timing']['calls'], 2)
|
||||
|
||||
|
||||
class DlsSafetyTest(unittest.TestCase):
|
||||
def test_rejected_refinement_retains_safe_candidate(self):
|
||||
@@ -373,5 +398,116 @@ class ClampedPathSafetyTest(unittest.TestCase):
|
||||
self.assertFalse(per['plans']['certified'].any().item())
|
||||
|
||||
|
||||
class SegmentCertificateTest(unittest.TestCase):
|
||||
class ThinObstacle(Kin):
|
||||
def __init__(self):
|
||||
self.dtype = torch.float64
|
||||
self.lo, self.hi = torch.full((6,), -5.0), torch.full((6,), 5.0)
|
||||
self.cont = torch.zeros(6, dtype=torch.bool)
|
||||
self.cert_groups = torch.tensor([0])
|
||||
self.cert_lever = torch.tensor([[1.0, 0, 0, 0, 0, 0]], dtype=self.dtype)
|
||||
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': ((q[:, :1] - 0.25).abs() - 0.05).clamp(max=cap)}
|
||||
|
||||
def test_bound_detects_hidden_collision_and_accepts_clear_segment(self):
|
||||
a, b = torch.zeros(3, 6, dtype=torch.float64), torch.zeros(3, 6, dtype=torch.float64)
|
||||
a[:, 0] = torch.tensor([0.0, -1.0, 0.25])
|
||||
b[:, 0] = torch.tensor([1.0, 0.0, 0.25])
|
||||
result = self.ThinObstacle().certify_segments(a, b)
|
||||
self.assertEqual(result['safe'].tolist(), [False, True, False])
|
||||
self.assertEqual(result['collision'].tolist(), [True, False, True])
|
||||
self.assertEqual(result['queries'].tolist(), [4, 2, 2])
|
||||
|
||||
def test_budget_and_depth_exhaustion_do_not_accept_unknown_segment(self):
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
b[:, 0] = 1.0
|
||||
kin = self.ThinObstacle()
|
||||
for kwargs in ({'max_queries': 2}, {'max_depth': 0}):
|
||||
with self.subTest(**kwargs):
|
||||
result = kin.certify_segments(a, b, **kwargs)
|
||||
self.assertFalse(result['safe'].item())
|
||||
self.assertTrue(result['unresolved'].item())
|
||||
self.assertEqual(result['queries'].item(), 2)
|
||||
|
||||
def test_budget_exhaustion_scales_with_gap_and_bound(self):
|
||||
class ConstantGap(self.ThinObstacle):
|
||||
def gaps(self, q, cap):
|
||||
return {'gm': torch.full((q.shape[0], 1), 0.1, dtype=q.dtype, device=q.device)}
|
||||
|
||||
# 界取 1 mm/度:1° 段要证明无碰撞,必须细分到位移界 < 两端间隙之和(0.2 mm)。
|
||||
kin = ConstantGap()
|
||||
kin.cert_lever = torch.ones(1, 6, dtype=torch.float64)
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
b[:, 0] = 1.0
|
||||
for budget, safe, queries in ((4, False, 3), (8, False, 5), (9, True, 9)):
|
||||
with self.subTest(budget=budget):
|
||||
result = kin.certify_segments(a, b, max_queries=budget)
|
||||
self.assertEqual(bool(result['safe'].item()), safe)
|
||||
self.assertEqual(int(result['queries'].item()), queries)
|
||||
|
||||
def test_tensor_budget_cannot_wrap_depth(self):
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
a[:, 0] = b[:, 0] = 0.25
|
||||
for tensor in (torch.tensor(255, dtype=torch.uint8), torch.tensor(255)):
|
||||
with self.subTest(dtype=tensor.dtype):
|
||||
with self.assertRaisesRegex(ValueError, 'plain ints'):
|
||||
self.ThinObstacle().certify_segments(a, b, max_depth=tensor)
|
||||
|
||||
def test_batch_matches_per_root_and_ignores_order(self):
|
||||
kin = self.ThinObstacle()
|
||||
a = torch.tensor([[0.0, 0, 0, 0, 0, 0], [0.25, 0, 0, 0, 0, 0], [-1.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
b = torch.tensor([[1.0, 0, 0, 0, 0, 0], [0.25, 0, 0, 0, 0, 0], [0.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
batch = kin.certify_segments(a, b, max_queries=16)
|
||||
self.assertEqual(batch['collision'].tolist(), [True, True, False])
|
||||
self.assertEqual(batch['safe'].tolist(), [False, False, True])
|
||||
for perm in ([0, 1, 2], [2, 1, 0], [1, 0, 2]):
|
||||
index = torch.tensor(perm)
|
||||
moved = kin.certify_segments(a[index], b[index], max_queries=16)
|
||||
for key in ('safe', 'collision', 'unresolved', 'queries'):
|
||||
torch.testing.assert_close(moved[key], batch[key][index], rtol=0, atol=0)
|
||||
|
||||
def test_empty_batch_and_all_rejected_frontier(self):
|
||||
kin = self.ThinObstacle()
|
||||
empty = kin.certify_segments(torch.zeros(0, 6, dtype=torch.float64), torch.zeros(0, 6, dtype=torch.float64))
|
||||
self.assertEqual(empty['safe'].shape, (0,))
|
||||
a = torch.tensor([[0.25, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
|
||||
result = kin.certify_segments(a, a.clone(), max_queries=2)
|
||||
self.assertEqual(result['safe'].tolist(), [False, False])
|
||||
self.assertEqual(result['collision'].tolist(), [True, False])
|
||||
self.assertEqual(result['outside_box'].tolist(), [False, True])
|
||||
|
||||
def test_nonfinite_and_outside_box_are_rejected(self):
|
||||
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
|
||||
kin = self.ThinObstacle()
|
||||
with patch.object(kin, 'gaps', side_effect=lambda q, cap: {'gm': torch.full((q.shape[0], 1), float('nan'))}):
|
||||
result = kin.certify_segments(a, b)
|
||||
self.assertFalse(result['safe'].item())
|
||||
self.assertTrue(result['unresolved'].item())
|
||||
for value in (float('inf'), -float('inf')):
|
||||
with self.subTest(value=value):
|
||||
with patch.object(kin, 'gaps',
|
||||
side_effect=lambda q, cap, v=value: {'gm': torch.full((q.shape[0], 1), v)}):
|
||||
result = kin.certify_segments(a, b)
|
||||
self.assertFalse(result['safe'].item())
|
||||
a[:, 0] = b[:, 0] = 6.0
|
||||
result = kin.certify_segments(a, b)
|
||||
self.assertFalse(result['safe'].item())
|
||||
self.assertTrue(result['outside_box'].item())
|
||||
|
||||
def test_point_segment_distance_is_symmetric_and_differentiable(self):
|
||||
p = torch.tensor([[0.0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], dtype=torch.float64, requires_grad=True)
|
||||
q = torch.tensor([[2.0, 0, 0]] * 4, dtype=torch.float64)
|
||||
point = torch.tensor([[1.0, 0, 0], [1, 3, 0], [4, 0, 0], [-1, 0, 0]], dtype=torch.float64)
|
||||
forward = seg_seg_dist(p, q, point, point)
|
||||
reverse = seg_seg_dist(point, point, p, q)
|
||||
torch.testing.assert_close(forward, torch.tensor([0.0, 3, 2, 1], dtype=torch.float64), atol=1e-8, rtol=0)
|
||||
torch.testing.assert_close(forward, reverse, atol=1e-8, rtol=0)
|
||||
forward.sum().backward()
|
||||
self.assertTrue(torch.isfinite(p.grad).all())
|
||||
torch.testing.assert_close(seg_seg_dist(p.detach(), p.detach(), point, point),
|
||||
(p.detach() - point).norm(dim=-1), atol=1e-8, rtol=0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user