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:
cbb
2026-10-20 19:13:40 +08:00
parent 95da49193f
commit 6de085fd37
10 changed files with 5960 additions and 5 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,658 @@
{
"segments": 48,
"kin_fp": "2ebcbf17",
"root_gap_cap_mm": 20.0,
"formula": "L = ceil(log2(bound_pair/(ga+gb)))\uff0c\u67e5\u8be2\u6570 = 2^L + 1\uff1bbound_pair \u53d6\u6700\u7d27\u7ec4\u5bf9\uff0c\u4e0d\u7528\u5168\u7ec4\u5bf9\u6700\u5927\u503c",
"levels_needed_by_status": {
"safe": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
2,
2,
2,
2,
3,
3,
3,
3,
3,
4,
4,
4,
4,
4
],
"unresolved": [
5,
5,
5,
5,
5,
6,
6,
6,
6,
6
],
"collision": [
4
]
},
"queries_needed_safe": [
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
3,
3,
3,
5,
5,
5,
5,
9,
9,
9,
9,
9,
17,
17,
17,
17,
17
],
"model_vs_measured_max_excess": 20,
"unresolved_needed_queries": [
33,
33,
33,
33,
33,
65,
65,
65,
65,
65
],
"collision_dense_check": [
{
"i": 180,
"start_it": 54,
"status": "collision",
"tight_group_pair": 5,
"bound_pair_mm": 29.09,
"endpoint_gap_sum_mm": 1.98854,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 5
}
],
"note": "\u6a21\u578b\u662f\u201c\u754c\u53ea\u968f\u4e8c\u5206\u51cf\u534a\u201d\u4e0b\u7684\u4e0b\u754c\uff1b\u5b9e\u6d4b\u67e5\u8be2\u53ef\u80fd\u66f4\u5c0f\uff08\u591a\u5c42\u91cc\u6709\u7684\u7ec4\u5bf9\u5148\u6ee1\u8db3\uff09\u6216\u66f4\u5927\uff08\u78b0\u649e\u6bb5\u63d0\u524d\u7ec8\u6b62\uff09\u3002",
"rows": [
{
"i": 0,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.382,
"endpoint_gap_sum_mm": 17.77168,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 0,
"start_it": 8,
"status": "unresolved",
"tight_group_pair": 13,
"bound_pair_mm": 8.2833,
"endpoint_gap_sum_mm": 0.25216,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 0,
"start_it": 91,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.459,
"endpoint_gap_sum_mm": 14.45459,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 0,
"start_it": 173,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 6.8776,
"endpoint_gap_sum_mm": 15.48695,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 21,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.7095,
"endpoint_gap_sum_mm": 12.81844,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 21,
"start_it": 100,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 1.3371,
"endpoint_gap_sum_mm": 0.03365,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 21,
"start_it": 127,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.5104,
"endpoint_gap_sum_mm": 0.68046,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 4
},
{
"i": 21,
"start_it": 282,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 3.5692,
"endpoint_gap_sum_mm": 1.72327,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 4
},
{
"i": 84,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 11.7042,
"endpoint_gap_sum_mm": 13.79742,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 84,
"start_it": 170,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.9891,
"endpoint_gap_sum_mm": 0.3999,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 18
},
{
"i": 84,
"start_it": 234,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 7.5939,
"endpoint_gap_sum_mm": 0.28698,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 31
},
{
"i": 84,
"start_it": 325,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 3.5738,
"endpoint_gap_sum_mm": 10.95173,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 128,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.4648,
"endpoint_gap_sum_mm": 16.52712,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 128,
"start_it": 31,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.1155,
"endpoint_gap_sum_mm": 14.24909,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 128,
"start_it": 62,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 0.3619,
"endpoint_gap_sum_mm": 12.62201,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 145,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.5691,
"endpoint_gap_sum_mm": 11.60111,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 145,
"start_it": 32,
"status": "safe",
"tight_group_pair": 4,
"bound_pair_mm": 4.6053,
"endpoint_gap_sum_mm": 0.67129,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 28
},
{
"i": 145,
"start_it": 138,
"status": "safe",
"tight_group_pair": 4,
"bound_pair_mm": 9.6062,
"endpoint_gap_sum_mm": 1.11765,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 15
},
{
"i": 145,
"start_it": 294,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 8.2956,
"endpoint_gap_sum_mm": 0.61574,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 17
},
{
"i": 164,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 12.0298,
"endpoint_gap_sum_mm": 11.50625,
"levels_needed": 1,
"queries_needed": 3,
"queries_used_batch32": 3
},
{
"i": 164,
"start_it": 84,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 18.0143,
"endpoint_gap_sum_mm": 1.7857,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 18
},
{
"i": 164,
"start_it": 182,
"status": "unresolved",
"tight_group_pair": 5,
"bound_pair_mm": 14.1522,
"endpoint_gap_sum_mm": 0.38976,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 164,
"start_it": 227,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 5.6457,
"endpoint_gap_sum_mm": 1.13586,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 10
},
{
"i": 175,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 6.5456,
"endpoint_gap_sum_mm": 15.18067,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 175,
"start_it": 46,
"status": "unresolved",
"tight_group_pair": 6,
"bound_pair_mm": 30.8668,
"endpoint_gap_sum_mm": 1.26529,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 32
},
{
"i": 175,
"start_it": 75,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 17.8885,
"endpoint_gap_sum_mm": 10.74989,
"levels_needed": 1,
"queries_needed": 3,
"queries_used_batch32": 3
},
{
"i": 175,
"start_it": 142,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 0.8185,
"endpoint_gap_sum_mm": 10.77208,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 180,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 3.4654,
"endpoint_gap_sum_mm": 14.04707,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 180,
"start_it": 14,
"status": "unresolved",
"tight_group_pair": 4,
"bound_pair_mm": 17.6609,
"endpoint_gap_sum_mm": 0.46562,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 180,
"start_it": 52,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 27.7231,
"endpoint_gap_sum_mm": 3.97272,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 15
},
{
"i": 180,
"start_it": 54,
"status": "collision",
"tight_group_pair": 5,
"bound_pair_mm": 29.09,
"endpoint_gap_sum_mm": 1.98854,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 5
},
{
"i": 180,
"start_it": 94,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.1032,
"endpoint_gap_sum_mm": 12.22495,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 197,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.0839,
"endpoint_gap_sum_mm": 6.87225,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 197,
"start_it": 80,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 3.3325,
"endpoint_gap_sum_mm": 0.50053,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 29
},
{
"i": 197,
"start_it": 82,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 5.7899,
"endpoint_gap_sum_mm": 0.16354,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 197,
"start_it": 211,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 18.6183,
"endpoint_gap_sum_mm": 5.55945,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 11
},
{
"i": 215,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.4256,
"endpoint_gap_sum_mm": 12.47554,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 215,
"start_it": 104,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.7208,
"endpoint_gap_sum_mm": 14.00267,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 215,
"start_it": 178,
"status": "unresolved",
"tight_group_pair": 6,
"bound_pair_mm": 25.7963,
"endpoint_gap_sum_mm": 1.07014,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 30
},
{
"i": 215,
"start_it": 215,
"status": "safe",
"tight_group_pair": 6,
"bound_pair_mm": 9.2152,
"endpoint_gap_sum_mm": 8.57509,
"levels_needed": 1,
"queries_needed": 3,
"queries_used_batch32": 3
},
{
"i": 224,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 6.492,
"endpoint_gap_sum_mm": 10.02154,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 224,
"start_it": 52,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 3.5989,
"endpoint_gap_sum_mm": 0.12354,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 32
},
{
"i": 224,
"start_it": 61,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.7788,
"endpoint_gap_sum_mm": 0.78961,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 5
},
{
"i": 224,
"start_it": 127,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.4884,
"endpoint_gap_sum_mm": 7.29024,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 255,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.5332,
"endpoint_gap_sum_mm": 11.184,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 255,
"start_it": 25,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 2.9831,
"endpoint_gap_sum_mm": 0.14423,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 29
},
{
"i": 255,
"start_it": 50,
"status": "safe",
"tight_group_pair": 4,
"bound_pair_mm": 7.2365,
"endpoint_gap_sum_mm": 0.61112,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 25
},
{
"i": 255,
"start_it": 181,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.2961,
"endpoint_gap_sum_mm": 0.74979,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 12
}
]
}
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"baseline_commit": "95da491",
"poses": "first 64 existing parity poses",
"n_poses": 64,
"kin_fp": "2ebcbf17",
"dtype": "float64",
"degenerate_capsules": [
686
],
"pairs_with_second_degenerate": 670,
"evaluated_pairs": 42880,
"changed_pairs_over_1e_8": 20661,
"old_minus_new_min_mm": 0.0,
"old_minus_new_max_mm": 86.00309853133399,
"new_reverse_max_abs_mm": 0.0,
"worst": {
"pose": 29,
"first_capsule": 430,
"second_capsule": 686,
"old_distance_mm": 493.354753665718,
"new_distance_mm": 407.35165513438403
},
"scope": "Raw capsule distances; does not imply these pairs win the group minimum."
}
@@ -0,0 +1,16 @@
{
"baseline_commit": "95da491",
"sample_source": "docs/experiment_round31_certificates.json",
"segments": 48,
"dense_points": 7586,
"max_step_deg": 0.01,
"cap_mm": 0.5,
"dtype": "float64",
"device": "cuda",
"kin_fp": "2ebcbf17",
"max_group_gap_difference_mm": 0.0,
"group_gaps_changed_over_1e_8": 0,
"minimum_gaps_changed_over_1e_8": 0,
"collision_bool_mismatches": 0,
"wall_s": 4.18259759999637
}
+10
View File
@@ -0,0 +1,10 @@
{
"n": 2000,
"fk_max_abs_mm": 4.547473508864641e-13,
"orient_max_abs": 3.3306690738754696e-16,
"gap_max_abs_mm": 2.5224267119483557e-13,
"collide_bool_mismatch": 0,
"feat_max_abs": 5.960463655974024e-08,
"grad_rel_err_max": 1.8474076070450906e-06,
"PASS": true
}
+21
View File
@@ -582,6 +582,27 @@
- **新增硬约束证据**:before/after 两组256题实际轨迹都出现超速,最大 **1.660004×VMAX**(来源是动作限幅后叠加0.3×gain2.2反馈)。实际各轴最大速度约 `[99.600,99.600,149.400,149.400,199.200,298.800]°/s`,并非只看原始指令推测。行程仍约参考2.425倍;速度、加速度和真实控制时延均未达部署验收。历史 `travel` 累积的是指令而非夹紧后位移,本轮已同时列出实际轨迹重算值,不悄悄换分母。 - **新增硬约束证据**:before/after 两组256题实际轨迹都出现超速,最大 **1.660004×VMAX**(来源是动作限幅后叠加0.3×gain2.2反馈)。实际各轴最大速度约 `[99.600,99.600,149.400,149.400,199.200,298.800]°/s`,并非只看原始指令推测。行程仍约参考2.425倍;速度、加速度和真实控制时延均未达部署验收。历史 `travel` 累积的是指令而非夹紧后位移,本轮已同时列出实际轨迹重算值,不悄悄换分母。
- **结论与下一步**:逐步限位一致性修复有效,未牺牲本批到达率;但加密审计仍检出段内碰撞,**不能部署、不能宣称零碰撞或50 Hz合格**。下一轮先验证更强的段内认证,再单独评估反馈后的速度/加速度约束与真实端到端调度;每次保持小样本单变量对照,继续保留失败证据。 - **结论与下一步**:逐步限位一致性修复有效,未牺牲本批到达率;但加密审计仍检出段内碰撞,**不能部署、不能宣称零碰撞或50 Hz合格**。下一轮先验证更强的段内认证,再单独评估反馈后的速度/加速度约束与真实端到端调度;每次保持小样本单变量对照,继续保留失败证据。
## 轮次 31:连续段认证的确定界与成本实测(exp: `exp/dagger-deploy`,判据变强,实测进不了 20 ms)
- **假设与判据**:轮次 30 的端点/中点检查漏掉段内碰撞(第180题 1.08–1.10 s,段内 −0.14927 mm,端点/中点均正)。若用可证明的全姿态运动界对整段做 Lipschitz 判据,应当(a) 在同一个漏检段上给出碰撞或拒绝,(b) 预算不足时只会拒绝而不会放行,(c) 不放宽 `>0` 判据。判据失败或耗时超预算都算负结论,必须实测不能外推。
- **确定界替代随机采样界**:JS 侧 `leverTable(4000)`/`leverBounds(20000)` 是随机姿态最大值再 ×1.05,不是全姿态上界。本轮按三角不等式给闭式界:组 g 的胶囊端点到关节 k(<g) 的最大位移 = (Σ_{j=k}^{g-2}‖P[j+1]−P[j]‖ + max_e‖e−P[g−1]‖)·|Δq_k|(k=g−1 时末项换成端点到该轴的垂直距离),组对 (ga,gb) 只累加 ga..gb−1,取组内最小仍保留 Lipschitz 性质。Python 端 `Kin.cert_lever`(15×6)即此表,未改 `lever`,也未改 JS。
- **界的量级**(STEP=[1.2,1.2,1.8,1.8,2.4,3.6]°):单整步在各组对上的位移界 **5.93–48.86 mm**,0 层通过要求端点间隙之和 > 界,即单端间隙至少 3.0–24.4 mm。实际执行段的间隙普遍只有 0.001–0.5 mm,所以“必须细分”是结构性的,不是实现缺陷。
- **基础求距 bug 与影响范围**:模型里有 1 个退化胶囊(索引 686,组 3)。旧 `seg_seg_dist` 在第二条线段退化成点时固定取该点为第二条起点,没把点投影到第一条线段上——[0,2] 段对点 1 的实测距离旧值 1 mm、反向调用约 1e−9 mm、真值 0。修复后在已有 2000 个对拍姿态的前 64 个姿态上,670 个“退化胶囊作第二条”的细对里 **20661/42880 个距离变小(旧值偏大,最大 86.003 mm)**,正反调用一致。但这批变化没有进入判定:把轮次 30 修复后 48 段的 7586 个 0.01° 加密点分别用修复前(95da491)与修复后内核重算,**组最小间隙逐位相同(最大差 0.0 mm)、碰撞布尔差 0**;`python -m ml.eval.check_parity` 仍 PASS(2000 样本,fk 4.55e−13 mm、间隙 2.52e−13 mm、特征 5.96e−08、梯度相对误差 1.85e−06、碰撞布尔差 0)。即这轮修复是正确的,但不解释轮次 30 的漏检,也不改变既有成绩。
- **认证实现**:`Kin.certify_segments` 为 float64 批量前沿二分,每根段独立查询预算,端点间隙用 `min(gap,cap)` 故钳制只会更保守;判据 `ga+gb > bound+2eps` 且两端 >eps,任一子段碰撞即整根判碰撞;超深度、超预算、非有限间隙、越出关节盒一律拒绝(`unresolved`/`outside_box`),不放行未知。宽相位 cap 可按本层界收到 `bound/2+eps`(`adaptive_cap`)。**尚未接入 rollout,未改查看器默认。**
- **48 段固定样本**(`docs/experiment_round31_certificates.json`,沿用轮次 30 的 12 题,每题取首/中/末移动段 + 原证书最小间隙段 + 显式指定 180:54;cap=20、eps=1e−4、max_depth=12):
| 每根查询预算 | 判安全 | 判碰撞 | 未知拒绝 | 查询数 p50/max |
|---|---|---|---|---|
| 2 | 20 | 0 | 28 | 2 / 2 |
| 8 | 26 | 1 | 21 | 4 / 8 |
| 32 | 37 | 1 | 10 | 4 / 32 |
- 已知漏检段(180 题执行段 54)**判碰撞,仅用 5 次查询**;该段原证书 `certified=true`、原内部 `col=0`。48 段里 0.01° 加密采样检出碰撞 1 段,认证接受该类段 **0 段**;整批与逐根调用结果逐位一致。
- 拒绝不等于安全:预算 2 时 28 段全部落 `unresolved`,实现不会把未知当通过。
- **成本模型对上实测**(`docs/experiment_round31_cert_cost_model.json`):按最紧组对的界,所需层数 `L=ceil(log2(bound_pair/(ga+gb)))`、查询数 `2^L+1`。37 个判安全段需要 2–17 次(L=0..4),10 个未知段需要 **33 或 65 次**(L=5..6),与预算 32 恰好卡在中间一致;碰撞段因提前撞到负间隙只用 5 次。结论:认证成本由 `bound/间隙` 的对数决定,**降低界或提高间隙余量**才是可行杠杆,加预算只是线性买单。
- **耗时负结论**(CUDA float64、B=1、驻留输入、每段4次预热+9次计时、变体按段交错,`docs/experiment_round31_adaptive_cap.json`):单段认证 **P50 22.93 ms、P90 58.43 ms、P99 101.57 ms,432 次里 225 次 > 20 ms**;自适应 cap 交错重测 P50 23.21 ms、P99 99.10 ms,**决策完全相同、耗时无收益**。先前顺序测得的“1.6×、整批 72→14.8 ms”经交错复测判定为 GPU 时钟/顺序漂移,不作为结论(48 根整批复测 260→212 ms 仍受同一偏差影响,且部署不会出现 48 根同时认证)。因此**连续认证按当前实现不能进 20 ms,更不能宣称 50 Hz**。
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl` **23 项通过**(轮次 30 为 14 项)。新增段认证 8 项覆盖:薄障碍段内碰撞被界抓到而清晰段被接受、预算/深度耗尽只拒不放、恒间隙合成段上预算 4/8/9 对应查询 3/5/9(验证细分层数与查询数的解析关系)、批量与逐根及乱序一致、空批与全拒前沿、NaN/±Inf 间隙拒绝、越界拒绝、退化距离对称性与梯度。**变异实测**:把类型门去掉后 `max_depth=torch.tensor(255,dtype=torch.uint8)` 会回绕成 `range(0)`、整段判据被跳过,真实间隙 −0.05 mm 的段直接返回 `safe=true`;加门后同一调用报 `certification budgets must be plain ints`。这是本轮抓到的第二个误放行路径(第一个是轮次 30 的未夹紧认证)。
- **口径与边界**:认证只在 float64 下进行,浮点求距误差没有形式上界(`seg_seg_dist` 距离下限约 1e−9 mm),eps=1e−4 mm 是工程容差不是证明;48 段取自轮次 30 修复后的执行轨迹,是固定样本不是新策略闭环;连续认证未接入 rollout,实际执行仍走端点/中点离散检查;速度/加速度超限(轮次 30 实测最大 1.66×VMAX)本轮未处理。
- **结论与下一步**:判据确实变强(抓到离散检查漏掉的段内碰撞、未知只拒不放),但成本与 `bound/间隙` 的对数绑定,**当前实现不能部署,也不得把“认证通过”说成零碰撞**。下一步两条:(1) 把逐层依赖的二分改成**一次均匀细分**(固定层数、单个 gaps 调用覆盖全部子段),其实时可行性取决于几何查询成本随批大小是否近似平坦;(2) 把余量要求移到规划/速度侧——实际轨迹贴着 0.001–0.02 mm 间隙走,任何认证方案都会退化成几十次查询,而反馈叠加后的速度约束本来就是未完成项。
## 分支清理(2026-10-20) ## 分支清理(2026-10-20)
- `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。 - `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。
+71 -1
View File
@@ -38,7 +38,8 @@ def seg_seg_dist(p1, q1, p2, q2, eps=1e-12):
s = torch.where(t_lo, (-c / a.clamp_min(eps)).clamp(0, 1), s) s = torch.where(t_lo, (-c / a.clamp_min(eps)).clamp(0, 1), s)
s = torch.where(t_hi, ((b - c) / a.clamp_min(eps)).clamp(0, 1), s) s = torch.where(t_hi, ((b - c) / a.clamp_min(eps)).clamp(0, 1), s)
t = t.clamp(0, 1) t = t.clamp(0, 1)
# 退化(线段长度≈0) # 第二条退化成点时,仍须在第一条上投影,而不是固定取其起点。
s = torch.where(e <= eps, (-c / a.clamp_min(eps)).clamp(0, 1), s)
s = torch.where(a <= eps, torch.zeros_like(s), s) s = torch.where(a <= eps, torch.zeros_like(s), s)
t = torch.where(e <= eps, torch.zeros_like(t), t) t = torch.where(e <= eps, torch.zeros_like(t), t)
c1 = p1 + d1 * s.unsqueeze(-1) c1 = p1 + d1 * s.unsqueeze(-1)
@@ -85,6 +86,25 @@ class Kin:
gi, gj = self.cap_g[self.pi], self.cap_g[self.pj] gi, gj = self.cap_g[self.pi], self.cap_g[self.pj]
self.pair_gp = torch.minimum(gi, gj) * 7 + torch.maximum(gi, gj) self.pair_gp = torch.minimum(gi, gj) * 7 + torch.maximum(gi, gj)
self.lever = T(K['lever']) self.lever = T(K['lever'])
table = [[0.0] * 7 for _ in range(6)]
for group in range(1, 7):
endpoints = [c[key] for c in caps if c['g'] == group for key in ('a', 'b')]
if not endpoints:
continue
pivot, axis = K['P'][group - 1], K['D'][group - 1]
unit = [v / math.hypot(*axis) for v in axis]
relative = [[e[j] - pivot[j] for j in range(3)] for e in endpoints]
radius = max(math.hypot(*v) for v in relative)
radial = max(math.hypot(v[1] * unit[2] - v[2] * unit[1],
v[2] * unit[0] - v[0] * unit[2],
v[0] * unit[1] - v[1] * unit[0]) for v in relative)
for joint in range(group):
chain = sum(math.dist(K['P'][j], K['P'][j + 1]) for j in range(joint, group - 1))
table[joint][group] = (chain + (radial if joint == group - 1 else radius)) * D2R
self.cert_groups = torch.tensor([a * 7 + b for a in range(7) for b in range(a + 2, 7)], device=device)
# 三角不等式给出全姿态界,不用随机姿态最大值冒充上界。
self.cert_lever = T([[table[k][b] if a <= k < b else 0.0 for k in range(6)]
for a in range(7) for b in range(a + 2, 7)])
# ---------------- 运动学 ---------------- # ---------------- 运动学 ----------------
def cum_xf(self, q): def cum_xf(self, q):
@@ -250,6 +270,56 @@ class Kin:
gm = gm.scatter_reduce(0, rep_b * len(gp) + self._gp49[self.pair_gp[pidx]], gap.clamp(max=cap), reduce='amin', include_self=True) gm = gm.scatter_reduce(0, rep_b * len(gp) + self._gp49[self.pair_gp[pidx]], gap.clamp(max=cap), reduce='amin', include_self=True)
return gm.view(B, len(gp)) return gm.view(B, len(gp))
@torch.no_grad()
def certify_segments(self, a, b, max_queries=32, max_depth=12, eps=1e-4, cap=20.0, adaptive_cap=True):
if self.dtype != torch.float64 or a.dtype != torch.float64 or b.dtype != torch.float64:
raise ValueError('segment certification requires float64')
# 预算给成张量时 max_depth + 1 会在 uint8 这类窄类型上回绕成 0,循环整个跳过,把碰撞段当安全返回。
if type(max_queries) is not int or type(max_depth) is not int:
raise ValueError('certification budgets must be plain ints')
if max_queries < 2 or max_depth < 0 or eps <= 0 or cap <= eps:
raise ValueError('invalid certification budget or tolerance')
count = a.shape[0]
status = torch.zeros(count, dtype=torch.long, device=a.device)
inside = (((a >= self.lo) & (a <= self.hi) & (b >= self.lo) & (b <= self.hi)) | self.cont).all(-1)
status[~inside] = 3
root = torch.arange(count, device=a.device)
gaps = self.gaps(torch.cat([a, b]), cap)['gm'][:, self.cert_groups]
ga, gb = gaps[:count], gaps[count:]
queries = torch.full((count,), 2, dtype=torch.long, device=a.device)
bound = (b - a).abs() @ self.cert_lever.T
for depth in range(max_depth + 1):
finite = torch.isfinite(ga).all(-1) & torch.isfinite(gb).all(-1)
collision = (ga <= 0).any(-1) | (gb <= 0).any(-1)
status[root[~finite]] = 2
status[root[collision]] = 1
positive = (ga > eps).all(-1) & (gb > eps).all(-1)
proven = positive & (ga + gb > bound + 2 * eps).all(-1)
pending = (status[root] == 0) & ~proven
if not bool(pending.any()):
break
a, b, ga, gb, root, bound = a[pending], b[pending], ga[pending], gb[pending], root[pending], bound[pending]
if depth == max_depth:
status[root] = 2
break
requested = torch.bincount(root, minlength=count)
status[(status == 0) & (queries + requested > max_queries)] = 2
keep = status[root] == 0
a, b, ga, gb, root, bound = (t[keep] for t in (a, b, ga, gb, root, bound))
if not root.numel():
break
mid = a + 0.5 * (b - a)
# 子段的界是父段一半;宽相位只需覆盖到能被证明的最小间隙,层越深筛得越狠。
qc = min(cap, float(bound.max()) / 2 + eps) if adaptive_cap else cap
gm = self.gaps(mid, qc)['gm'][:, self.cert_groups]
queries += torch.bincount(root, minlength=count)
a, b = torch.cat([a, mid]), torch.cat([mid, b])
ga, gb = torch.cat([ga, gm]), torch.cat([gm, gb])
root = torch.cat([root, root])
bound = torch.cat([bound, bound]) / 2
return {'safe': status == 0, 'collision': status == 1, 'unresolved': status == 2,
'outside_box': status == 3, 'queries': queries}
def min_gap(self, q, cap=40.0): def min_gap(self, q, cap=40.0):
return self.gaps(q, cap)['gm'].min(-1).values return self.gaps(q, cap)['gm'].min(-1).values
+126 -3
View File
@@ -153,11 +153,124 @@ def replay_dense(path, indices, max_step_deg=0.05, device='cpu', out=None):
return result return result
@torch.no_grad()
def replay_certificates(path, indices, device='cpu', out=None, repeats=5, extra_segments=()):
saved = torch.load(path, map_location='cpu', weights_only=True)
kin = Kin(device=device, dtype=torch.float64)
if saved.get('kin_fp') != kin.fp:
raise ValueError('trace/kin fingerprint mismatch')
if not indices or min(indices) < 0 or max(indices) >= len(saved['tasks']) or repeats < 1:
raise ValueError('valid task indices and positive repeats required')
rows, offset = [], 0
for _, per, count in saved['batches']:
for index in indices:
if not offset <= index < offset + count:
continue
b = index - offset
nstep = min(per['traj'].shape[1] - 1, math.floor((float(per['limit'][b]) + 1e-6) / DT))
if torch.isfinite(per['t_done'][b]):
nstep = min(nstep, round(float(per['t_done'][b]) / DT))
traj = per['traj'][b, :nstep + 1]
moving = ((traj[1:] - traj[:-1]).abs().max(-1).values > 1e-6).nonzero().flatten().tolist()
if not moving:
continue
plans = per['plans']
nearest, nearest_gap = None, float('inf')
for it in moving:
pi = int(torch.searchsorted(plans['it'], it, right=True)) - 1
step = it - int(plans['it'][pi])
if 0 <= step < plans['commit'].shape[2] and plans['certified'][b, pi, step]:
gap = float(plans['gaps'][b, pi, [step, plans['commit'].shape[2] + step]].min())
if gap < nearest_gap:
nearest, nearest_gap = it, gap
selected = {moving[0], moving[len(moving) // 2], moving[-1]}
if nearest is not None:
selected.add(nearest)
for task_index, it in extra_segments:
if task_index == index:
if it not in moving:
raise ValueError('extra segment must be a moving segment before deadline or arrival')
selected.add(it)
for it in sorted(selected):
rows.append({'i': index, 'start_it': it, 'a': traj[it].tolist(), 'b': traj[it + 1].tolist()})
offset += count
if not rows:
raise ValueError('no moving segments in selected tasks')
a = torch.tensor([r['a'] for r in rows], dtype=torch.float64, device=device)
b = torch.tensor([r['b'] for r in rows], dtype=torch.float64, device=device)
started = time.perf_counter()
print('certificate replay: %d tasks, %d fixed moving segments' % (len(indices), len(rows)), flush=True)
summaries = {}
for budget in (2, 8, 32):
result = kin.certify_segments(a, b, max_queries=budget)
summaries[str(budget)] = {key: int(result[key].sum()) for key in ('safe', 'collision', 'unresolved', 'outside_box')}
summaries[str(budget)]['queries_p50'] = float(result['queries'].double().quantile(0.5))
summaries[str(budget)]['queries_max'] = int(result['queries'].max())
for j, row in enumerate(rows):
row.setdefault('budgets', {})[str(budget)] = {key: value[j].item() for key, value in result.items()}
print('budget %d: %s' % (budget, summaries[str(budget)]), flush=True)
latencies = []
for j, row in enumerate(rows):
aa, bb = a[j:j + 1], b[j:j + 1]
discrete = kin.gaps(torch.cat([aa, aa + 0.5 * (bb - aa), bb]), 0.5)['gm'].min(-1).values
row['previous_midpoint_endpoint_gap_mm'] = discrete.tolist()
nseg = max(2, 2 * math.ceil(float((bb - aa).abs().max()) / 0.02))
alpha = torch.linspace(0, 1, nseg + 1, dtype=torch.float64, device=device)
points = aa + alpha[:, None] * (bb - aa)
dense = torch.cat([kin.gaps(chunk, 0.5)['gm'].min(-1).values for chunk in points.split(64)])
row['dense_points'] = len(points)
row['dense_min_gap_mm'] = float(dense.min())
row['dense_min_fraction'] = float(alpha[dense.argmin()])
for _ in range(3):
kin.certify_segments(aa, bb)
elapsed = []
for _ in range(repeats):
if a.is_cuda:
torch.cuda.synchronize(a.device)
t0 = time.perf_counter()
result = kin.certify_segments(aa, bb)
if a.is_cuda:
torch.cuda.synchronize(a.device)
elapsed.append((time.perf_counter() - t0) * 1000)
row['batch_single_match'] = all(value.item() == row['budgets']['32'][key] for key, value in result.items())
row['latency_ms'] = elapsed
latencies.extend(elapsed)
print('segment %d/%d task %d it %d dense %.6f mm queries %d latency %.2f ms' %
(j + 1, len(rows), row['i'], row['start_it'], row['dense_min_gap_mm'],
row['budgets']['32']['queries'], sum(elapsed) / len(elapsed)), flush=True)
elapsed = torch.tensor(latencies, dtype=torch.float64)
result = {
'trace': str(path), 'tasks_sha256': saved.get('tasks_sha256'), 'kin_fp': kin.fp,
'device': str(device), 'gpu': torch.cuda.get_device_name(a.device) if a.is_cuda else None,
'torch_version': str(torch.__version__), 'cpu_threads': torch.get_num_threads(), 'dtype': 'float64',
'indices': indices, 'extra_segments': list(extra_segments),
'selection': 'First/middle/last moving segment and minimum original certified gap per task; plus explicit extra_segments.',
'cap_mm': 20.0, 'eps_mm': 1e-4, 'max_depth': 12, 'dense_max_step_deg': 0.01,
'timing': {'scope': 'B=1 certificate only, resident inputs, 3 warmups per segment; synchronized wall time, not policy or scheduling latency.',
'repeats_per_segment': repeats, 'calls': len(latencies),
'p50_ms': float(elapsed.quantile(0.5)), 'p99_ms': float(elapsed.quantile(0.99)),
'max_ms': float(elapsed.max()), 'over_20ms': int((elapsed > 20).sum())},
'budgets': summaries,
'dense_collision_segments': sum(r['dense_min_gap_mm'] <= 0 for r in rows),
'accepted_dense_collision_segments': sum(r['dense_min_gap_mm'] <= 0 and r['budgets']['32']['safe'] for r in rows),
'batch_single_match': all(r['batch_single_match'] for r in rows),
'scope': 'Fixed recorded segments, not a new rollout. Dense replay is finite sampling; floating-point certification error is not formally bounded.',
'wall_s': time.perf_counter() - started, 'rows': rows,
}
if out:
with open(out, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, allow_nan=False)
return result
def main(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument('--ckpt') ap.add_argument('--ckpt')
ap.add_argument('--trace') ap.add_argument('--trace')
ap.add_argument('--dense_indices', help='逗号分隔的任务下标;回放实际轨迹,不依赖原 col 标记') ap.add_argument('--dense_indices', help='逗号分隔的任务下标;回放实际轨迹,不依赖原 col 标记')
ap.add_argument('--cert_indices', help='固定实际段的连续认证、加密对照和单段计时')
ap.add_argument('--cert_segments', default='', help='额外指定任务号:零基段号,逗号分隔')
ap.add_argument('--repeats', type=int, default=5)
ap.add_argument('--max_step_deg', type=float, default=0.05) ap.add_argument('--max_step_deg', type=float, default=0.05)
ap.add_argument('--samples', default='ml/data/samples') ap.add_argument('--samples', default='ml/data/samples')
ap.add_argument('--split', default='val') ap.add_argument('--split', default='val')
@@ -169,14 +282,24 @@ def main():
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu') ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
ap.add_argument('--out', default=None) ap.add_argument('--out', default=None)
a = ap.parse_args() a = ap.parse_args()
if a.dense_indices and a.cert_indices:
ap.error('--dense_indices and --cert_indices are mutually exclusive')
if a.cert_segments and not a.cert_indices:
ap.error('--cert_segments requires --cert_indices')
if a.trace: if a.trace:
if a.dense_indices: if a.cert_indices:
extra = [tuple(map(int, segment.split(':'))) for segment in a.cert_segments.split(',') if segment]
indices = [int(i) for i in a.cert_indices.split(',')]
if any(len(segment) != 2 or segment[0] not in indices for segment in extra):
ap.error('--cert_segments requires task:segment pairs from --cert_indices')
replay_certificates(a.trace, indices, a.device, a.out, a.repeats, extra)
elif a.dense_indices:
replay_dense(a.trace, [int(i) for i in a.dense_indices.split(',')], a.max_step_deg, a.device, a.out) replay_dense(a.trace, [int(i) for i in a.dense_indices.split(',')], a.max_step_deg, a.device, a.out)
else: else:
replay_first_hits(a.trace, a.out) replay_first_hits(a.trace, a.out)
return return
if a.dense_indices: if a.dense_indices or a.cert_indices:
ap.error('--dense_indices requires --trace') ap.error('--dense_indices and --cert_indices require --trace')
if not a.ckpt: if not a.ckpt:
ap.error('--ckpt or --trace is required') ap.error('--ckpt or --trace is required')
+137 -1
View File
@@ -9,8 +9,9 @@ from unittest.mock import patch
import torch import torch
from ml.eval.eval_cl import json_finite, run 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.sim.closed_loop import TeacherOracle, rollout
from ml.common.kin_torch import Kin, seg_seg_dist
class EvalMetricsTest(unittest.TestCase): class EvalMetricsTest(unittest.TestCase):
@@ -124,6 +125,30 @@ class EvalMetricsTest(unittest.TestCase):
self.assertGreater(row['min_gap64_mm'], 0) self.assertGreater(row['min_gap64_mm'], 0)
self.assertEqual(row['steps'], 1) 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): class DlsSafetyTest(unittest.TestCase):
def test_rejected_refinement_retains_safe_candidate(self): def test_rejected_refinement_retains_safe_candidate(self):
@@ -373,5 +398,116 @@ class ClampedPathSafetyTest(unittest.TestCase):
self.assertFalse(per['plans']['certified'].any().item()) 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__': if __name__ == '__main__':
unittest.main() unittest.main()