exp: 轮次29——全拒后减半反馈到达55.1%,补齐无碰撞指标与首撞回放

512题无碰撞到达54.7%、碰撞1.8%;9次首撞均在J5限位且双精度复现。保留小样本消融,撤回未经实际调度验证的50Hz达标结论,附六项回归测试及完整配置结果。
This commit is contained in:
cbb
2026-10-20 17:06:53 +08:00
parent ce9edd7934
commit 9eaff7341f
7 changed files with 1227 additions and 44 deletions
+62
View File
@@ -0,0 +1,62 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 512,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round29_rows.json",
"out": "docs/experiment_round29.json",
"trace": "ml/runs/round29_trace.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "1fd72f71eaf8170b45782beede0d91544e1f7142e98c0642560af814382f01ef",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 512,
"n_ref": 512,
"success": 0.55078125,
"collision_free_success": 0.546875,
"time_ok_1p3": 0.361328125,
"collision_rate": 0.017578125,
"collision_steps_mean": 0.1015625,
"travel_ratio_median": 2.425084352493286,
"time_mean_s": 2.322056770324707,
"brakes_mean": 19.228515625,
"start_fail": 0.0,
"goal_ratio_2s": 0.25053170323371887,
"err_fail_med": 194.51763916015625,
"err_success_med": 0.1961677223443985,
"dls_share": 0.634765625,
"time_budget": "teacher×2(≥3 s)"
},
"simulation_wall_s": 488.4280448999925
}
+779
View File
@@ -0,0 +1,779 @@
{
"trace": "ml/runs/round29_trace.pt",
"n": 512,
"reported_collision_tasks": 9,
"replayed_first_hits32": 9,
"replayed_first_hits64": 9,
"scope": "Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.",
"rows": [
{
"i": 21,
"task": {
"tid": 1223280991,
"id": "s0001_000564",
"cat": "A",
"mode": "pose",
"q0": [
178.767,
101.917,
25.283,
31.588,
-12.179,
-6.464
],
"T": [
320.779,
79.294,
350.853
],
"R": [
-0.5548626629953249,
-0.1382477148217311,
-0.820374910976869,
-0.29956278115972873,
-0.8867511636570162,
0.35204334093514156,
-0.7761375943462505,
0.44108949556693955,
0.4506112421399604
],
"exec_s": 2.86,
"travel": 581.023,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.659999847412109,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.35770511627197266,
-0.3957242965698242
],
"gap64_mm": [
0.5,
0.5,
0.3577231796451059,
-0.39572636491366353
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
139.78468322753906,
87.256591796875,
6.928672790527344,
103.78778839111328,
-94.0,
19.4688720703125
],
"endpoint": [
139.50450134277344,
87.4175796508789,
6.71871280670166,
104.29376983642578,
-94.0,
21.218488693237305
],
"dense_nsegments": 175,
"dense_gap64_min_mm": -0.39572636491366353
},
{
"i": 84,
"task": {
"tid": 1268963905,
"id": "s0002_000538",
"cat": "B",
"mode": "pose",
"q0": [
-49.198,
52.395,
108.796,
75.534,
82.302,
-121.823
],
"T": [
-85.062,
275.378,
888.929
],
"R": [
0.5139412462799992,
-0.4285630099475821,
-0.7431003578769356,
-0.5533754886400825,
-0.8275489878032067,
0.09454227286372051,
-0.6554692700230204,
0.36262435010704513,
-0.6624678231921397
],
"exec_s": 3.2600000000000002,
"travel": 349.09,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.980000019073486,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.030836105346679688,
-0.1565418243408203,
-0.11790847778320312
],
"gap64_mm": [
0.5,
0.030838235678215753,
-0.15655898804830848,
-0.11791701257236653
],
"pair32": [
0,
6,
6,
6
],
"pair64": [
0,
6,
6,
6
],
"boundary_joints": [
5
],
"previous": [
-69.87329864501953,
36.559112548828125,
2.094588279724121,
102.22724151611328,
89.0,
-212.1793212890625
],
"endpoint": [
-69.79582214355469,
37.351837158203125,
-0.033895254135131836,
102.45509338378906,
89.0,
-214.75440979003906
],
"dense_nsegments": 258,
"dense_gap64_min_mm": -0.16882067924099076
},
{
"i": 145,
"task": {
"tid": 593213945,
"id": "s0003_000414",
"cat": "B",
"mode": "pose",
"q0": [
-49.395,
84.783,
2.744,
14.841,
-86.783,
81.932
],
"T": [
5.391,
506.723,
383.367
],
"R": [
-0.5269859333114162,
-0.7554822816510317,
-0.38927156099983334,
0.6402732880654818,
-0.05172298162073713,
-0.7664038424760652,
0.5588702377848656,
-0.6531242265324508,
0.5109724082909718
],
"exec_s": 2.96,
"travel": 560.55,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.599999904632568,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.3180522918701172,
-0.19827747344970703
],
"gap64_mm": [
0.5,
0.5,
0.318071359587325,
-0.19825289671655533
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
-36.05080032348633,
49.852294921875,
22.054914474487305,
67.3380126953125,
-94.0,
79.87274932861328
],
"endpoint": [
-35.60316848754883,
48.9223747253418,
21.386226654052734,
67.87915802001953,
-94.0,
79.27398681640625
],
"dense_nsegments": 93,
"dense_gap64_min_mm": -0.19825289671655533
},
{
"i": 164,
"task": {
"tid": 1743026973,
"id": "s0003_000814",
"cat": "C",
"mode": "pos",
"q0": [
-65.982,
5.595,
-64.669,
-72.912,
85.732,
-86.377
],
"T": [
-189.354,
221.034,
302.457
],
"R": null,
"exec_s": 2.3000000000000003,
"travel": 306.824,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.0399999618530273,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.5,
-0.12672901153564453
],
"gap64_mm": [
0.5,
0.5,
0.5,
-0.12673094091899273
],
"pair32": [
0,
0,
0,
5
],
"pair64": [
0,
0,
0,
5
],
"boundary_joints": [
5
],
"previous": [
-33.6672248840332,
39.63850021362305,
-60.174774169921875,
-108.2870864868164,
88.27082061767578,
-74.56200408935547
],
"endpoint": [
-32.8936882019043,
39.478729248046875,
-58.639915466308594,
-107.65913391113281,
89.0,
-74.92655944824219
],
"dense_nsegments": 154,
"dense_gap64_min_mm": -0.12673094091899273
},
{
"i": 175,
"task": {
"tid": 444406422,
"id": "s0003_001100",
"cat": "B",
"mode": "pos",
"q0": [
-28.854,
43.33,
-137.874,
-6.895,
-88.001,
-43.745
],
"T": [
379.105,
293.88,
653.541
],
"R": null,
"exec_s": 2.58,
"travel": 465.057,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 0.8399999737739563,
"dls_it": 144,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.4698514938354492,
-0.6612262725830078,
-0.9359521865844727
],
"gap64_mm": [
0.5,
0.4698356780126254,
-0.6612412101671552,
-0.9359581587234
],
"pair32": [
0,
6,
6,
6
],
"pair64": [
0,
6,
6,
6
],
"boundary_joints": [
5
],
"previous": [
-42.48794937133789,
11.756612777709961,
-124.7530288696289,
-46.12489700317383,
-94.0,
-54.57075500488281
],
"endpoint": [
-42.73639678955078,
10.6148681640625,
-124.47537231445312,
-48.52891159057617,
-94.0,
-55.451263427734375
],
"dense_nsegments": 241,
"dense_gap64_min_mm": -0.9544276230224238
},
{
"i": 180,
"task": {
"tid": 737760595,
"id": "s0003_001280",
"cat": "B",
"mode": "pos",
"q0": [
41.213,
49.353,
-149.808,
-8.981,
-88.566,
83.45
],
"T": [
118.145,
2.733,
266.082
],
"R": null,
"exec_s": 1.78,
"travel": 228.67,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 1.1399999856948853,
"dls_it": 102,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.06517887115478516,
-0.0172576904296875,
0.013133049011230469
],
"gap64_mm": [
0.5,
0.06521150921177998,
-0.017227151210720493,
0.013145878955246104
],
"pair32": [
0,
5,
5,
5
],
"pair64": [
0,
5,
5,
5
],
"boundary_joints": [
5
],
"previous": [
1.8212896585464478,
-9.327469825744629,
-150.87704467773438,
-23.00735855102539,
-94.0,
93.73664855957031
],
"endpoint": [
1.3598560094833374,
-9.66404914855957,
-150.52992248535156,
-23.200042724609375,
-94.0,
92.69589233398438
],
"dense_nsegments": 105,
"dense_gap64_min_mm": -0.020246891242656773
},
{
"i": 215,
"task": {
"tid": 169730876,
"id": "s0004_000544",
"cat": "B",
"mode": "pos",
"q0": [
47.051,
-1.179,
-94.31,
-43.128,
67.067,
-119.867
],
"T": [
-142.696,
-139.159,
726.163
],
"R": null,
"exec_s": 2.16,
"travel": 461.684,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.5399999618530273,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.5,
-0.08817481994628906
],
"gap64_mm": [
0.5,
0.5,
0.5,
-0.08817155079043282
],
"pair32": [
0,
0,
0,
6
],
"pair64": [
0,
0,
0,
6
],
"boundary_joints": [
5
],
"previous": [
34.57734298706055,
32.23564910888672,
-71.76512908935547,
10.783411979675293,
88.44934844970703,
-105.16633605957031
],
"endpoint": [
34.31901931762695,
33.24732208251953,
-72.12022399902344,
10.171412467956543,
89.0,
-106.89080810546875
],
"dense_nsegments": 173,
"dense_gap64_min_mm": -0.08817155079043282
},
{
"i": 266,
"task": {
"tid": 458057184,
"id": "s0005_000271",
"cat": "B",
"mode": "pos",
"q0": [
44.394,
32.402,
-145.074,
-48.37,
-85.543,
13.576
],
"T": [
184.38,
-306.445,
433.207
],
"R": null,
"exec_s": 2.52,
"travel": 376.205,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 1.2200000286102295,
"dls_it": 304,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.2372283935546875,
-0.05915641784667969
],
"gap64_mm": [
0.5,
0.5,
0.23719947210205383,
-0.05918908805933398
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
20.49032974243164,
10.35391902923584,
-172.87489318847656,
-73.93492126464844,
-93.70915985107422,
14.725995063781738
],
"endpoint": [
20.34522819519043,
10.012239456176758,
-172.91363525390625,
-74.4600601196289,
-94.0,
13.266871452331543
],
"dense_nsegments": 146,
"dense_gap64_min_mm": -0.05918908805933398
},
{
"i": 325,
"task": {
"tid": 1206824904,
"id": "s0005_001755",
"cat": "B",
"mode": "pose",
"q0": [
-68.225,
58.481,
-31.386,
57.2,
-31.831,
-80.259
],
"T": [
30.805,
353.752,
661.419
],
"R": [
0.5493387160259536,
0.13571391775309405,
0.8245051289125273,
-0.09525700289583322,
-0.9701190077749987,
0.22314841283987508,
0.8301524429129026,
-0.2011239500451364,
-0.5199962290670785
],
"exec_s": 2.34,
"travel": 659.756,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.0999999046325684,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.3628082275390625,
0.004294395446777344,
-0.6861848831176758
],
"gap64_mm": [
0.5,
0.3628058862147938,
0.004307730814957722,
-0.686172816414107
],
"pair32": [
0,
18,
5,
5
],
"pair64": [
0,
18,
5,
5
],
"boundary_joints": [
5
],
"previous": [
-39.524147033691406,
54.68175506591797,
-18.574026107788086,
103.78845977783203,
-94.0,
-53.657958984375
],
"endpoint": [
-39.42676544189453,
55.778987884521484,
-18.88759994506836,
103.97470092773438,
-94.0,
-54.84250259399414
],
"dense_nsegments": 119,
"dense_gap64_min_mm": -0.686172816414107
}
]
}
+46 -6
View File
@@ -5,6 +5,13 @@
评测约定:val 任务、部署节奏(r = 3 重规划、1 段采样、K=8);「纯策略」= 关安全层;「部署」= 开安全层。时延:桌面 RTX 3090 上 `sample_k(B=64, K=8)` 墙钟均值(均值,非 P99);浏览器 ONNX P99 以 §9.2 R4 的独立测量为准。 评测约定:val 任务、部署节奏(r = 3 重规划、1 段采样、K=8);「纯策略」= 关安全层;「部署」= 开安全层。时延:桌面 RTX 3090 上 `sample_k(B=64, K=8)` 墙钟均值(均值,非 P99);浏览器 ONNX P99 以 §9.2 R4 的独立测量为准。
**口径更正(2026-10-20,轮次 29 复核;下列历史原始数值保留):**
- 轮次 24–28 的“50 Hz 可行/连续无饥饿”结论撤回。同步 `await plan(); tick()` 不是实际 20 ms 调度;`starveTicks` 只是耗时除以 20 ms 的估算。组件分位数相加不等于整次规划 P99,实测 62–68 ms 也超过 commit=3 的 60 ms。仍须严格认证下的真查看器 R9 测量,且不放松 §9.2 的 P99 < 20 ms。
- Python 端点/中点检查不等于连续认证;JS `--seg 0` 是放松安全的消融,不是部署验收。开安全层碰撞必须为 0,不能套用“纯策略 <3%”判据。既有到达率允许途中碰撞,新指标 `collision_free_success` 才是时限内无碰撞到达率。
- `replan=empty` 是同步仿真的零前缀配方,不是已验证的异步执行配置。所有任务(含 D)继续保留在历史对照分母内;不能与 held-out 可达任务 ≥98% 直接比较。
- 轮次 26 的几何角速度雅可比不等于远离目标时 skew 姿态残差的精确导数,因而既改变控制律又消除差分误差;不能把全部收益归因于去噪。3.58% 是单个位置雅可比样本对拍,不是普遍噪声界。`check_dls.py` 中标 B=256 的测量实际仅有 64 行,不能引用为 B=256 性能。
- `err_fail_med` 是批次结束时未按时到达任务的末端误差,不是每题截止时误差;仿真墙钟/题也不是单次规划延迟。查看器默认选择器仍为 consist,endpoint 仅是可选实验。
## 轮次 0:基线(2026-10-18,main) ## 轮次 0:基线(2026-10-18,main)
- **假设**:模型开环已拟合老师(末端 ~6 mm),闭环失败另有原因。 - **假设**:模型开环已拟合老师(末端 ~6 mm),闭环失败另有原因。
@@ -498,18 +505,51 @@
- DLS 兜底半步(全拒时朝目标走一步并认证):52.3%(基线 52.0%),碰撞 1.6→2.7%,无收益;重复多步版本碰撞 45.7%(第二步未认证),已回退为单步且默认关闭。 - DLS 兜底半步(全拒时朝目标走一步并认证):52.3%(基线 52.0%),碰撞 1.6→2.7%,无收益;重复多步版本碰撞 45.7%(第二步未认证),已回退为单步且默认关闭。
- 增益调度(远 2.8/近 2.2):47.3% / time_ok 32.8%,等同 gain2.8 常数;高增益在远处就造成过冲,按距离衰减无效。 - 增益调度(远 2.8/近 2.2):47.3% / time_ok 32.8%,等同 gain2.8 常数;高增益在远处就造成过冲,按距离衰减无效。
- **浏览器同步**:`select='endpoint'` 进 Runner/bench(`--select endpoint`),冷启动跳过 8 份前瞻(省 ~80 ms P99,冷启动价值评分被端点排序替代)。nc10 bench(--seg 0 --select endpoint):pos ok 4/5 reached、pose ok 2/4,刹车率 32.1%/25.5%(consist 48.1%/31.6%),planP50 31.9/36.5 ms、P99 63.5/67.5 ms。 - **浏览器同步**:`select='endpoint'` 进 Runner/bench(`--select endpoint`),冷启动跳过 8 份前瞻(省 ~80 ms P99,冷启动价值评分被端点排序替代)。nc10 bench(--seg 0 --select endpoint):pos ok 4/5 reached、pose ok 2/4,刹车率 32.1%/25.5%(consist 48.1%/31.6%),planP50 31.9/36.5 ms、P99 63.5/67.5 ms。
- **50 Hz 部署口径实测(Runner 增加 `starveTicks`:ceil(planMs/20) − 规划时队列步数)**: - **同步 bench 耗时与队列耗尽估算(不是 50 Hz 调度实测)**:Runner 的 `starveTicks = max(0,ceil(planMs/20) − 规划时队列步数)`,不含真实 tick 时间戳。
| 集合(endpoint, commit=2, --seg 0) | 到达 ok/reached | 刹车率 | planP50/P99 | 饥饿 tick/次规划 | | 集合(endpoint, commit=2, --seg 0) | 到达 ok/reached | 刹车率 | planP50/P99 | 估算饥饿 tick/次规划 |
|---|---|---|---|---| |---|---|---|---|---|
| near_column_pos (10) | 4/5 | 32.7% | 32.0/62.1 ms | 0.87 | | near_column_pos (10) | 4/5 | 32.7% | 32.0/62.1 ms | 0.87 |
| near_column_pose (10) | 2/3 | 25.6% | 36.8/68.0 ms | 0.78 | | near_column_pose (10) | 2/3 | 25.6% | 36.8/68.0 ms | 0.78 |
| route40 (20) | 12/13 | 40.4% | 44.3/68.1 ms | 1.26 | | route40 (20) | 12/13 | 40.4% | 44.3/68.1 ms | 1.26 |
- commit=2 的队列只有 40 ms,route 集 planP50 44 ms 已经超过 → 每次规划平均空转 ~1.3 tick(50 Hz 下机械臂会"走两步停一下")。 - commit=2 对应 40 ms 队列,route 集 planP50 44 ms 已超过;估算提示可能饥饿,但该 bench 等规划完成才 tick,没有观测实际空转。
- **commit=3(60 ms 队列)可覆盖 P50 44 ms 与多数 P99**:endpoint n=256 → 47.7% / 27.0% / 1.2% / 0.245;gain2.5 反而降到 45.7% / 24.6%。**连续 50 Hz 口径选 commit=3(47.7%),极限准确率选 commit=2(52.1%,偶发空转)**。 - commit=3 对应 60 ms,仍低于上述 P99,不能据此宣称连续 50 Hz。其 Python 同步结果:endpoint n=256 → 47.7% / 27.0% / 1.2% / 0.245;gain2.5 为 45.7% / 24.6%。
- **结论**:**新历史最佳(50 Hz 节奏,n=512)= 端点选择 + 解析 J + gain2.2 + commit=2 + safety_partial:到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285**。相对本会话开始的历史最佳(42.2% / 29.1% / 2.9% / 0.270)到达 +9.9 pp、时间效率 +6.1 pp。commit=1 上限 59.4%(碰撞 3.9%,超 3% 线,且规划频率不可实时)。 - **结论(经轮次 29 更正)**:端点选择 + 解析 J + gain2.2 + commit=2 + safety_partial,在 n=512 同步仿真为 **到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285**。历史 42.2% 使用 commit=1 与 FD,非单变量对照。commit=1 到达 59.4% 但碰撞 3.9%;无论 2.0% 还是 3.9%,开安全层均未通过零碰撞要求,实时性亦未通过。
- **下一步**:① 碰撞 2.0% 的来源分解(端点选择使候选更贴障碍?);② 把 `select=endpoint` 写进查看器默认(Runner 默认仍 consist);③ route 集 safety P50 30 ms 是 planP50 的主项,继续压认证成本(候选去重/按需投影已做,剩余在 collideFree 本身)。 - **下一步**:先查碰撞来源,保持严格认证和查看器默认;再测真实异步调度及认证成本,不因 Python 收益直接切换部署默认。
## 轮次 29:安全层全拒后减弱目标反馈(exp: `exp/dagger-deploy`)
- **假设**:目标反馈将绕障候选推向障碍,连续全拒会反复停在同处;仅在上一计划被全拒时减小下一计划反馈,可能保留正常段收敛速度并减少重复刹停。
- **判据**:同任务、同 seed、同 K、同认证下到达和 time_ok 不下降,碰撞不增加;部署门槛仍是零碰撞及真实时延达标。所有实验不重训、不扩大数据。
- **固定配方**:`python -m ml.eval.eval_cl --n 256 --K 8 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --seed 0`;dag2 checkpoint、几何雅可比。下表仅改列出的参数,默认 `fb_block_gain=1,dls_safety=0`。512 题按两个 256 批、每批 seed=0,沿用轮次 28 顺序。
| 改动(n=256) | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|---|---|---|---|---|---|
| 原配置复跑 | 52.7% | 37.5% | 2.3% | 0.210 | 34.6 |
| `--dls_safety 1` | 52.7% | 37.5% | 2.0% | 0.210 | 34.6 |
| `--fb_clear 20 --fb_cmin 0.3` | 32.0% | 5.9% | 1.2% | 0.323 | 3.2 |
| `--sub_refresh 1` | 51.6% | 35.5% | 2.0% | 0.279 | 33.8 |
| `--select adapt` | 52.7% | 37.9% | 2.3% | 0.210 | 34.6 |
| `--fb_block_gain 0.3` | 56.2% | 38.7% | 3.1% | 0.178 | 17.2 |
| `--fb_block_gain 0.5` | **57.4%** | **39.1%** | 3.1% | 0.210 | 19.3 |
| `--fb_block_gain 0.7` | 55.5% | 38.3% | 2.3% | 0.189 | 25.0 |
| `--fb_block_gain 0.5 --dls_safety 1` | **57.4%** | **39.1%** | 2.7% | 0.210 | 19.2 |
- **单独减半反馈 n=512**:到达 55.1% / time_ok 36.1% / 碰撞 2.5% / ratio2s 0.251 / 刹停 19.5;相对轮次 28 同规模基线 52.1% / 35.2% / 2.0% / 0.285 / 35.7,到达 +3.0 pp、碰撞也 +0.5 pp,不满足“碰撞不增加”判据。
- **为何不采用其他变体**:所有近障碍状态都衰减反馈会明显拖慢到达;子目标重预测无收益;adapt 全拒后旧计划本来就失效,一致性分数全为 0,实质变成等分选首条而非“继续绕行”。
- **原配置失败分解(n=256)**:135 题按时到达,121 题失败(其中 20 题后来才到,101 题始终没到)。A/B/C/D/Dr 分别 128/74/32/13/9 题,B/C 失败题刹停中位数约 96/93 次;D 留在分母,不用删题提高成绩。
- **碰撞定位**:原配置共 6 题碰撞,只有 224 号首次碰撞紧跟 DLS 接管;另外 5 题首次碰撞早于接管或根本未接管。为 DLS 加端点/中点检查仅减少 1 题,不能解释剩余碰撞。候选认证的未夹紧累加点与执行的逐步夹紧点不同是待验证假设,尚不能断言根因。
- **计时**:n=256 减半反馈+DLS检查整批墙钟 243.6 s,单独减半 n=512 为 443.4 s;这些不是规划时延,不能证明 50 Hz。反馈条件分支未增加几何查询,但其实际延迟收益未测。
- **记录改进**:评估输出完整参数、checkpoint/任务 SHA256、批大小和随机种子规则;增加时限内无碰撞到达率;修正 n_ref、参考时间占比和条件指标的跨批汇总。可保存实际轨迹供不重新采样的回放,NaN 在 JSON 中写为 null,执行进度每 50 步输出。
- **回归检查**:`python -m unittest tests.test_eval_cl -v` 六项通过,覆盖跨批分母、空条件集、非有限数、首撞探针正对照、精修被拒后保留安全候选、只有全拒后才减弱反馈。精修对照在关闭检查时确实碰撞,打开后保留候选,证明门会执行;这是合成测试,不替代真实几何验收。
- **仍未修复的边界**:`fb_fallback=0` 保持关闭;非部分前缀分支仍有“只验一步却重复执行”问题,不能开启部署。速度限制在反馈叠加前而非叠加后,尚未验证速度/加速度符合硬约束。
- **减半反馈 + DLS 检查的 n=512 复核(完整输出 `docs/experiment_round29.json`)**:到达 **282/512 = 55.1%**、时限内无碰撞到达 **280/512 = 54.7%**、time_ok **36.1%**、碰撞 **9/512 = 1.8%**、ratio2s **0.251**、刹停 **19.23/题**。到达题末端误差中位数 **0.196 mm**;有参考且到达题的行程比中位数 **2.425**(远高于 1.10 要求)。整批墙钟 **488.4 s**,非控制延迟。
- 完整命令:`python -u -m ml.eval.eval_cl --n 512 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --fb_block_gain 0.5 --dls_safety 1 --out docs/experiment_round29.json --dump ml/runs/round29_rows.json --trace ml/runs/round29_trace.pt`。
- 与轮次 28 同规模端点基线相比,到达 +3.0 pp、time_ok +0.9 pp、碰撞少 1 题(10→9),不足以宣称稳定安全改善;n=256 碰撞仍由 6→7 题。
- **首撞回放(`docs/experiment_round29_collisions.json`)**:`python -m ml.eval.diag_collision --trace ml/runs/round29_trace.pt --out docs/experiment_round29_collisions.json`。9 题首次碰撞在 CPU float32/float64 均复现,均位于 **J5 限位**;q0 与首次碰撞前一点均为正间隙,首撞采样点负间隙约 **−0.017 至 −0.936 mm**。这排除了这 9 次碰撞由起点本就碰撞或 float32 单独翻号造成,但还需要记录原认证点才能证明夹紧的因果作用;0.01° 加密仅覆盖首撞段,不是整条轨迹安全审计。
- 双精度运动学/间隙另与已有 JS 2000 个样本对拍:`python -m ml.eval.check_parity` PASS,碰撞布尔差 0、间隙最大差 2.53e−13 mm。首撞回放仍共用碰撞模型,不替代真机或独立连续认证。
- **结论与下一步**:减少连续刹停确有小样本收益,但碰撞、行程和真实时延仍未过门槛;保留为实验选项,不更改查看器默认。下一轮记录原认证点与 J5 夹紧后实际点,修复后用同批同 seed A/B 和独立细采样复核,禁止靠关掉认证提速。
## 分支清理(2026-10-20) ## 分支清理(2026-10-20)
+55 -1
View File
@@ -25,9 +25,58 @@ def pair_names():
return {a * 7 + b: f'{nm(a)}-{nm(b)}' for a in range(7) for b in range(a + 1, 7)} return {a * 7 + b: f'{nm(a)}-{nm(b)}' for a in range(7) for b in range(a + 1, 7)}
@torch.no_grad()
def replay_first_hits(path, out=None):
saved = torch.load(path, map_location='cpu', weights_only=True)
k32, k64 = Kin(), Kin(dtype=torch.float64)
rows, offset = [], 0
for _, per, count in saved['batches']:
for b in (per['col'] > 0).nonzero().flatten().tolist():
it = int(round(float(per['first_hit'][b]) / DT))
prev, q = per['traj'][b, it - 1:it + 1]
mid = prev + 0.5 * (q - prev)
points = torch.stack([per['traj'][b, 0], prev, mid, q])
gm32 = k32.gaps(points, 0.5)['gm']
gm64 = k64.gaps(points.double(), 0.5)['gm']
gap32, gp32 = gm32.min(-1)
gap64, gp64 = gm64.min(-1)
nseg = max(1, math.ceil(float((q - prev).abs().max()) / 0.01))
dense = prev.double() + torch.linspace(0, 1, nseg + 1, dtype=torch.float64)[:, None] * (q.double() - prev.double())
dense_gap = torch.cat([k64.gaps(chunk, 0.5)['gm'].min(-1).values for chunk in dense.split(32)])
boundary = (~k32.cont) & ((q - k32.lo).abs().lt(1e-5) | (q - k32.hi).abs().lt(1e-5))
row = {
'i': offset + b, 'task': saved['tasks'][offset + b],
'first_hit_s': float(per['first_hit'][b]), 'dls_it': int(per['dls_it'][b]),
'point_order': ['q0', 'previous', 'midpoint', 'endpoint'],
'gap32_mm': gap32.tolist(), 'gap64_mm': gap64.tolist(),
'pair32': gp32.tolist(), 'pair64': gp64.tolist(),
'boundary_joints': (boundary.nonzero().flatten() + 1).tolist(),
'previous': prev.tolist(), 'endpoint': q.tolist(),
'dense_nsegments': nseg, 'dense_gap64_min_mm': float(dense_gap.min()),
}
rows.append(row)
print('task %d first_hit %.2fs gap32 %s gap64 %s boundary %s' %
(row['i'], row['first_hit_s'], row['gap32_mm'], row['gap64_mm'], row['boundary_joints']), flush=True)
offset += count
result = {
'trace': path, 'n': offset, 'reported_collision_tasks': len(rows),
'replayed_first_hits32': sum(min(r['gap32_mm'][2:]) <= 0 for r in rows),
'replayed_first_hits64': sum(min(r['gap64_mm'][2:]) <= 0 for r in rows),
'scope': 'Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.',
'rows': rows,
}
if out:
with open(out, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, allow_nan=False)
print('replayed first hits: %d/%d float32, %d/%d float64' %
(result['replayed_first_hits32'], len(rows), result['replayed_first_hits64'], len(rows)), flush=True)
return result
def main(): def main():
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True) ap.add_argument('--ckpt')
ap.add_argument('--trace')
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')
ap.add_argument('--n', type=int, default=512) ap.add_argument('--n', type=int, default=512)
@@ -38,6 +87,11 @@ 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.trace:
replay_first_hits(a.trace, a.out)
return
if not a.ckpt:
ap.error('--ckpt or --trace is required')
ck = torch.load(a.ckpt, map_location=a.device) ck = torch.load(a.ckpt, map_location=a.device)
model = build(ck['cfg']).to(a.device) model = build(ck['cfg']).to(a.device)
+77 -25
View File
@@ -1,32 +1,57 @@
"""小规模闭环评估(部署口径): """小规模同步闭环评估;仿真墙钟不等于控制延迟。"""
python -m ml.eval.eval_cl --n 256 --commit 3 --fb 1.8 --partial [--ki 0.1 --select consist] import argparse, functools, hashlib, json, math, time
输出:到达 / 碰撞 / ratio2s / 刹停 / 精修接管 / 墙钟。直接调 rollout,参数与部署一致。""" from pathlib import Path
import argparse, functools, json, time
import torch import torch
from ml.common.ckpt import load_policy from ml.common.ckpt import load_policy
from ml.common.kin_torch import Kin from ml.common.kin_torch import Kin
from ml.data import load_tasks from ml.data import load_tasks
from ml.sim.closed_loop import rollout from ml.sim.closed_loop import rollout, tasks_to_tensors
import ml.sim.closed_loop as clmod import ml.sim.closed_loop as clmod
def run(model, kin, tasks, device, bs=256, **kw): def run(model, kin, tasks, device, bs=256, **kw):
agg, per = {}, [] per = []
for i in range(0, len(tasks), bs): for i in range(0, len(tasks), bs):
r, p = rollout(model, kin, tasks[i:i + bs], device, **kw) t0 = time.perf_counter()
print('batch %d-%d/%d start' % (i + 1, min(i + bs, len(tasks)), len(tasks)), flush=True)
def progress(step, total):
print('batch %d step %d/%d wall %.1fs' % (i // bs + 1, step, total, time.perf_counter() - t0), flush=True)
r, p = rollout(model, kin, tasks[i:i + bs], device, progress=progress, **kw)
per.append((r, p, len(tasks[i:i + bs]))) per.append((r, p, len(tasks[i:i + bs])))
n = sum(w for _, _, w in per) print('batch %d done wall %.1fs' % (i // bs + 1, time.perf_counter() - t0), flush=True)
for r, _, w in per: flat = {k: torch.cat([p[k] for _, p, _ in per]) for k in per[0][1] if k != 'traj'}
for k, v in r.items(): _, _, _, ex, tr = tasks_to_tensors(tasks, 'cpu')
if k in ('n', 'time_budget') or v is None: continue ok, col = flat['ok'], flat['col'] > 0
agg[k] = agg.get(k, 0.0) + v * w has_ref = torch.isfinite(ex)
out = {k: v / n for k, v in agg.items()}; out['n'] = n okr = ok & has_ref & torch.isfinite(tr)
out['goal_ratio_2s'] = torch.cat([p['goal_ratio_2s'] for _, p, _ in per]).median().item() median = lambda x: x.median().item() if x.numel() else None
out['err_fail_med'] = torch.cat([p['err'] for _, p, _ in per])[~torch.cat([p['ok'] for _, p, _ in per])].median().item() out = {
out['dls_share'] = torch.cat([p['dls_it'] for _, p, _ in per]).ge(0).float().mean().item() 'n': len(tasks), 'n_ref': int(has_ref.sum()),
out['time_budget'] = per[0][0]['time_budget'] 'success': ok.float().mean().item(),
return out 'collision_free_success': (ok & ~col).float().mean().item(),
'time_ok_1p3': (ok & has_ref & (flat['t_done'] <= 1.3 * ex)).sum().item() / int(has_ref.sum()) if has_ref.any() else None,
'collision_rate': col.float().mean().item(),
'collision_steps_mean': flat['col'].mean().item(),
'travel_ratio_median': median(flat['travel'][okr] / tr[okr].clamp(min=1)),
'time_mean_s': flat['t_done'][ok].mean().item() if ok.any() else None,
'brakes_mean': flat['brakes'].mean().item(),
'start_fail': flat['start_fail'].float().mean().item(),
'goal_ratio_2s': median(flat['goal_ratio_2s']),
'err_fail_med': median(flat['err'][~ok]),
'err_success_med': median(flat['err'][ok]),
'dls_share': flat['dls_it'].ge(0).float().mean().item(),
'time_budget': ('fixed %.3f s' % kw['max_sec']) if kw.get('max_sec') is not None else
(per[0][0]['time_budget'] if len({r['time_budget'] for r, _, _ in per}) == 1 else 'mixed'),
}
return out, per
def json_finite(value):
if isinstance(value, dict): return {k: json_finite(v) for k, v in value.items()}
if isinstance(value, list): return [json_finite(v) for v in value]
if isinstance(value, float) and not math.isfinite(value): return None
return value
def main(): def main():
@@ -48,6 +73,14 @@ def main():
ap.add_argument('--fb_every', type=int, default=1) ap.add_argument('--fb_every', type=int, default=1)
ap.add_argument('--safety_try', type=int, default=0) ap.add_argument('--safety_try', type=int, default=0)
ap.add_argument('--sel_w', type=float, default=0.1) ap.add_argument('--sel_w', type=float, default=0.1)
ap.add_argument('--dls_safety', type=int, default=0)
ap.add_argument('--fb_clear', type=float, default=0.0)
ap.add_argument('--fb_cmin', type=float, default=0.3)
ap.add_argument('--sub_refresh', type=int, default=0)
ap.add_argument('--fb_block_gain', type=float, default=1.0)
ap.add_argument('--dump', default=None)
ap.add_argument('--out', default=None)
ap.add_argument('--trace', default=None)
ap.add_argument('--select', default='consist') ap.add_argument('--select', default='consist')
ap.add_argument('--replan', default='r3') ap.add_argument('--replan', default='r3')
ap.add_argument('--safety', type=int, default=1, help='1=安全层+部分前缀;0=关') ap.add_argument('--safety', type=int, default=1, help='1=安全层+部分前缀;0=关')
@@ -57,6 +90,7 @@ def main():
ap.add_argument('--dls', default='analytic', choices=['analytic', 'fd']) ap.add_argument('--dls', default='analytic', choices=['analytic', 'fd'])
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')
a = ap.parse_args() a = ap.parse_args()
print('config ' + json.dumps(vars(a), ensure_ascii=False), flush=True)
if a.dls == 'fd': if a.dls == 'fd':
clmod.dls_steps = functools.partial(clmod.dls_steps, analytic=False) clmod.dls_steps = functools.partial(clmod.dls_steps, analytic=False)
model, ck = load_policy(a.ckpt, a.device) model, ck = load_policy(a.ckpt, a.device)
@@ -65,17 +99,35 @@ def main():
tasks = ([json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()] if a.tasks tasks = ([json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()] if a.tasks
else [t for t in load_tasks([a.samples]) if t['split'] == a.split]) else [t for t in load_tasks([a.samples]) if t['split'] == a.split])
tasks = tasks[:a.n] tasks = tasks[:a.n]
if not tasks: ap.error('no tasks selected')
meta = {
'config': vars(a), 'batch_size': 256, 'batch_seed_rule': 'same seed per batch',
'checkpoint_sha256': hashlib.sha256(Path(a.ckpt).read_bytes()).hexdigest(),
'tasks_sha256': hashlib.sha256(json.dumps(tasks, sort_keys=True).encode()).hexdigest(),
'kin_fp': kin.fp, 'act_scale': float(model.act_scale),
}
t0 = time.perf_counter() t0 = time.perf_counter()
r = run(model, kin, tasks, a.device, K=a.K, safety=bool(a.safety), seed=a.seed, max_sec=a.max_sec, r, pers = run(model, kin, tasks, a.device, K=a.K, safety=bool(a.safety), seed=a.seed, max_sec=a.max_sec,
safety_partial=(a.commit if (a.safety and a.partial) else 0), commit_n=a.commit, 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, 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) 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,
return_traj=bool(a.trace))
wall = time.perf_counter() - t0 wall = time.perf_counter() - t0
print('ckpt=%s n=%d K=%d commit=%d fb=%.2f ki=%.2f select=%s replan=%s safety=%d' % result = {**meta, 'metrics': r, 'simulation_wall_s': wall}
(a.ckpt, r['n'], a.K, a.commit, a.fb, a.ki, a.select, a.replan, a.safety)) print(json.dumps(json_finite(result), ensure_ascii=False, allow_nan=False), flush=True)
print('success %.3f time_ok_1p3 %.3f coll %.3f ratio2s %.3f brakes %.1f dls_share %.3f err_fail %.2f wall %.1fs (%.0f ms/题)' if a.out:
% (r['success'], r['time_ok_1p3'] or 0, r['collision_rate'], r['goal_ratio_2s'], r['brakes_mean'], Path(a.out).write_text(json.dumps(json_finite(result), indent=2, ensure_ascii=False, allow_nan=False) + '\n', encoding='utf-8')
r['dls_share'], r['err_fail_med'], wall, wall / max(1, len(tasks)) * 1000)) if a.dump:
rows = []
keys = ['done', 'ok', 't_done', 'limit', 'col', 'brakes', 'err', 'first_hit', 'dls_it', 'goal_ratio_2s']
flat = {k: torch.cat([p[k] for _, p, _ in pers]).tolist() for k in keys}
for i, t in enumerate(tasks):
rows.append({**t, 'i': i, **{k: flat[k][i] for k in keys}})
Path(a.dump).write_text(json.dumps(json_finite(rows), allow_nan=False), encoding='utf-8')
print('dump', a.dump, flush=True)
if a.trace:
torch.save({**meta, 'tasks': tasks, 'batches': pers}, a.trace)
print('trace', a.trace, flush=True)
if __name__ == '__main__': if __name__ == '__main__':
+46 -12
View File
@@ -66,11 +66,10 @@ def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5, analytic=True):
xf = kin.cum_xf(x) xf = kin.cum_xf(x)
e0 = err(x, T, Rt, pose, xf) e0 = err(x, T, Rt, pose, xf)
if analytic: if analytic:
# J = −∂err/∂q:位置 err = T−p → +Jv;姿态 err = 100·rotvec(Rt·Rqᵀ) → +100·Jw # 几何角速度 Jw 仅在姿态误差接近零时等于该 skew 残差的负导数。
Jv, Jw = kin.jac(x, xf) Jv, Jw = kin.jac(x, xf)
J = torch.cat([Jv, 100 * Jw * pose[:, None, None]], 1) # [B,6(err),6(q)] J = torch.cat([Jv, 100 * Jw * pose[:, None, None]], 1)
else: else:
# 2026-10-20:6 次串行有限差分合并为一次批量 FK(数值等价,单次规划 ~25ms→~7ms)
dx = torch.zeros(B, 6, 6, device=q.device); dx[:, ar, ar] = 1e-3 dx = torch.zeros(B, 6, 6, device=q.device); dx[:, ar, ar] = 1e-3
xp = (x[:, None, :] + dx).reshape(B * 6, 6) 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)) ep = err(xp, T.repeat_interleave(6, 0), Rt.repeat_interleave(6, 0), pose.repeat_interleave(6, 0))
@@ -153,7 +152,8 @@ class TeacherOracle:
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None, def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None,
qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross', 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_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): 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,
progress=None):
"""use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头; """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头;
qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放; qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放;
select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。 select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。
@@ -174,6 +174,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
last = None; last_ok = torch.zeros(B, dtype=torch.bool, device=device) last = None; last_ok = torch.zeros(B, dtype=torch.bool, device=device)
fb_i = torch.zeros(B, 6, device=device) # 积分项:累积持续同向的目标反馈(慢变偏差) fb_i = torch.zeros(B, 6, device=device) # 积分项:累积持续同向的目标反馈(慢变偏差)
fb_cache = None; pit = 0 # fb_every>1:复用上一次规划的目标反馈(误差方向慢变) fb_cache = None; pit = 0 # fb_every>1:复用上一次规划的目标反馈(误差方向慢变)
last_brake = torch.zeros(B, dtype=torch.bool, device=device)
subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device) 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() sub_t = torch.zeros(B, device=device); sub_prev = q0.clone()
done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device) done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device)
@@ -203,6 +204,14 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
else: else:
z = torch.randn(B * K, H, 6, device=device, generator=g) * z_scale z = torch.randn(B * K, H, 6, device=device, generator=g) * z_scale
act, val, sub = model.sample_k(feat, K, z, steps=steps) act, val, sub = model.sample_k(feat, K, z, steps=steps)
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)
inc = act.float().clamp(-1, 1) * astep inc = act.float().clamp(-1, 1) * astep
if fb_gain > 0: # 实验(2026-10-20):目标反馈加在所有候选上(安全层也评估带反馈的候选) if fb_gain > 0: # 实验(2026-10-20):目标反馈加在所有候选上(安全层也评估带反馈的候选)
if fb_cache is None or fb_every <= 1 or pit % fb_every == 0: if fb_cache is None or fb_every <= 1 or pit % fb_every == 0:
@@ -210,6 +219,10 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
else: else:
fb = fb_cache fb = fb_cache
pit += 1 pit += 1
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)
if fb_ki > 0: if fb_ki > 0:
fb_i = fb_decay * fb_i + fb[:, 0] * (~done)[:, None] fb_i = fb_decay * fb_i + fb[:, 0] * (~done)[:, None]
lim = step * fb_imax lim = step * fb_imax
@@ -218,9 +231,14 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
elif fb_scale > 0: # 距离衰减:远离目标全增益,贴近目标减弱(减少过冲/刹停) elif fb_scale > 0: # 距离衰减:远离目标全增益,贴近目标减弱(减少过冲/刹停)
dg, _ = goal_err(kin, qe, T, R) dg, _ = goal_err(kin, qe, T, R)
gdec = fb_gain * torch.clamp(dg / fb_scale, fb_near, 1.0) gdec = fb_gain * torch.clamp(dg / fb_scale, fb_near, 1.0)
if gm_ is not None: gdec = gdec * gm_
inc = inc + gdec[:, None, None, None] * fb[:, :, None, :] inc = inc + gdec[:, None, None, None] * fb[:, :, None, :]
else: else:
inc = inc + fb_gain * fb[:, :, None, :] 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, :]
if cold: if cold:
need = sub[:, 12] > 0.5 need = sub[:, 12] > 0.5
subs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180 subs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
@@ -237,10 +255,16 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6] fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6]
if select == 'first': if select == 'first':
score = torch.zeros(B, K, device=device); score[:, 0] = 1 score = torch.zeros(B, K, device=device); score[:, 0] = 1
elif select == 'endpoint': elif select in ('endpoint', 'adapt'):
qa = fut[:, :, -1].reshape(B * K, 6) qa = fut[:, :, -1].reshape(B * K, 6)
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0)) e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
score = -e_end.view(B, K) score = -e_end.view(B, K)
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)
elif cold or select == 'value': elif cold or select == 'value':
# 短前瞻:候选末态的价值 # 短前瞻:候选末态的价值
qa = fut[:, :, -1].reshape(B * K, 6); qb = fut[:, :, -2].reshape(B * K, 6) qa = fut[:, :, -1].reshape(B * K, 6); qb = fut[:, :, -2].reshape(B * K, 6)
@@ -272,6 +296,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
if collect is not None: # 反馈蒸馏:收集与部署完全一致的 (特征, 复合动作=模型输出+目标反馈) if collect is not None: # 反馈蒸馏:收集与部署完全一致的 (特征, 复合动作=模型输出+目标反馈)
collect.append((feat.detach().float().cpu(), inc[ar, pick].detach().float().cpu())) collect.append((feat.detach().float().cpu(), inc[ar, pick].detach().float().cpu()))
committed = torch.ones(B, dtype=torch.bool, device=device) committed = torch.ones(B, dtype=torch.bool, device=device)
braked = torch.zeros(B, dtype=torch.bool, device=device)
if safety and safety_partial: # 2026-10-20:允许「部分安全前缀」——执行到第一个不安全步为止,而不是全拒刹停 if safety and safety_partial: # 2026-10-20:允许「部分安全前缀」——执行到第一个不安全步为止,而不是全拒刹停
maxlen = torch.zeros(B, dtype=torch.long, device=device) maxlen = torch.zeros(B, dtype=torch.long, device=device)
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone() chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
@@ -297,7 +322,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
mskf = (maxlen == 0) & ok1 mskf = (maxlen == 0) & ok1
commit = torch.where(mskf[:, None, None], fbcom, commit) commit = torch.where(mskf[:, None, None], fbcom, commit)
msk = msk | mskf[:, None] msk = msk | mskf[:, None]
brakes += (maxlen == 0).float() brakes += (maxlen == 0).float(); braked = maxlen == 0
pick = chosen_k; committed = maxlen > 0 pick = chosen_k; committed = maxlen > 0
elif safety: elif safety:
ok_any = torch.zeros(B, dtype=torch.bool, device=device) ok_any = torch.zeros(B, dtype=torch.bool, device=device)
@@ -316,17 +341,24 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
fb1 = dls_steps(kin, qe, T, R, n=1) * fb_fallback fb1 = dls_steps(kin, qe, T, R, n=1) * fb_fallback
ok1 = kin.gaps(qe + fb1[:, 0], 0.5)['gm'].min(-1).values > 0 ok1 = kin.gaps(qe + fb1[:, 0], 0.5)['gm'].min(-1).values > 0
commit = torch.where((~ok_any & ok1)[:, None, None], fb1.expand(-1, commit_n, -1), commit) commit = torch.where((~ok_any & ok1)[:, None, None], fb1.expand(-1, commit_n, -1), commit)
brakes += (~ok_any).float() brakes += (~ok_any).float(); braked = ~ok_any
pick = chosen_k; committed = ok_any pick = chosen_k; committed = ok_any
if near.any(): if near.any():
ref = dls_steps(kin, qe, T, R, n=commit_n) ref = dls_steps(kin, qe, T, R, n=commit_n)
commit = torch.where(near[:, None, None], ref, commit) take = near
dls_it = torch.where(near & (dls_it < 0), torch.full_like(dls_it, it), dls_it) if dls_safety and safety: # 2026-10-20:近目标 DLS 接管也要过安全层(部署端 _certCommit 本来就会验;不过则退回候选计划)
committed = committed & ~near pts = qe[:, None] + torch.cumsum(ref, 1)
mids = torch.cat([qe[:, None], pts[:, :-1]], 1) + 0.5 * ref
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
okd = (kin.gaps(allq, 0.5)['gm'].min(-1).values > 0).view(B, 2 * commit_n).all(-1)
take = near & okd
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(F5):last 记真正提交的那条候选(安全层换了候选时是换后的那条);刹停 / DLS 接管的行无效 # 2026-10-19(F5):last 记真正提交的那条候选(安全层换了候选时是换后的那条);刹停 / DLS 接管的行无效
if fb_ki > 0: # 刹停/精修接管时清积分(防止原地累积到限幅后突然冲出去) if fb_ki > 0: # 刹停/精修接管时清积分(防止原地累积到限幅后突然冲出去)
fb_i = torch.where(committed[:, None], fb_i, torch.zeros_like(fb_i)) fb_i = torch.where(committed[:, None], fb_i, torch.zeros_like(fb_i))
last = torch.cat([qe[:, None], fut[ar, pick]], 1); last_ok = committed last = torch.cat([qe[:, None], fut[ar, pick]], 1); last_ok = committed; last_brake = braked
queue = torch.cat([queue, commit], 1) queue = torch.cat([queue, commit], 1)
cold = False cold = False
d = queue[:, 0]; queue = queue[:, 1:] d = queue[:, 0]; queue = queue[:, 1:]
@@ -348,6 +380,8 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
if it + 1 == k2s: e2 = e.clone() if it + 1 == k2s: e2 = e.clone()
newly = (~done) & (e < 1) & (oe < 0.5) newly = (~done) & (e < 1) & (oe < 0.5)
t_done = torch.where(newly, torch.full_like(t_done, tn), t_done); done |= newly t_done = torch.where(newly, torch.full_like(t_done, tn), t_done); done |= newly
if progress is not None and (it == 0 or (it + 1) % 50 == 0 or it + 1 == nmax):
progress(it + 1, nmax)
if done.all(): break if done.all(): break
ok = done & (t_done <= limit + 1e-6) ok = done & (t_done <= limit + 1e-6)
if moved_start is None: moved_start = travel.clone() if moved_start is None: moved_start = travel.clone()
+162
View File
@@ -0,0 +1,162 @@
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import torch
from ml.eval.eval_cl import json_finite, run
from ml.eval.diag_collision import replay_first_hits
from ml.sim.closed_loop import TeacherOracle, rollout
class EvalMetricsTest(unittest.TestCase):
def setUp(self):
self.tasks = [
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': 1, 'travel': 5},
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': None, 'travel': None},
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': 2, 'travel': 6},
]
self.per = {k: torch.tensor(v) for k, v in {
'ok': [True, False, True], 'done': [True, False, True],
't_done': [0.5, float('nan'), 2.0], 'col': [1.0, 0.0, 0.0],
'travel': [6.0, 0.0, 8.0], 'brakes': [2.0, 4.0, 6.0],
'start_fail': [False, True, False], 'goal_ratio_2s': [0.3, 0.9, 0.1],
'err': [0.2, 200.0, 0.8], 'dls_it': [4, -1, 20],
}.items()}
def evaluate(self, tasks, bs=2, **kw):
offset = 0
def fake_rollout(model, kin, batch, device, **kwargs):
nonlocal offset
p = {k: v[offset:offset + len(batch)] for k, v in self.per.items()}
offset += len(batch)
return {'time_budget': 'mixed'}, p
with patch('ml.eval.eval_cl.rollout', side_effect=fake_rollout), contextlib.redirect_stdout(io.StringIO()):
return run(None, None, tasks, 'cpu', bs=bs, **kw)[0]
def test_global_denominators_and_collision_free_arrival(self):
result = self.evaluate(self.tasks)
self.assertEqual(result['n_ref'], 2)
self.assertAlmostEqual(result['success'], 2 / 3, places=6)
self.assertAlmostEqual(result['collision_free_success'], 1 / 3, places=6)
self.assertAlmostEqual(result['collision_rate'], 1 / 3, places=6)
self.assertEqual(result['time_ok_1p3'], 1.0)
self.assertEqual(result['time_mean_s'], 1.25)
self.assertAlmostEqual(result['travel_ratio_median'], 1.2, places=6)
self.assertAlmostEqual(result['goal_ratio_2s'], 0.3, places=6)
self.assertEqual(result['err_fail_med'], 200.0)
self.assertEqual(result['brakes_mean'], 4.0)
def test_empty_conditional_metrics_and_explicit_limit(self):
task = {**self.tasks[0], 'exec_s': None, 'travel': None}
result = self.evaluate([task], max_sec=0.04)
self.assertIsNone(result['err_fail_med'])
self.assertIsNone(result['time_ok_1p3'])
self.assertIsNone(result['travel_ratio_median'])
self.assertEqual(result['time_budget'], 'fixed 0.040 s')
self.assertEqual(result['collision_free_success'], 0.0)
def test_json_replaces_nonfinite_without_changing_zero(self):
value = {'items': [float('nan'), float('inf'), -float('inf'), 0.0]}
text = json.dumps(json_finite(value), allow_nan=False)
self.assertEqual(json.loads(text), {'items': [None, None, None, 0.0]})
def test_first_hit_probe_detects_collision(self):
class PlaneKin:
def __init__(self, **kwargs):
self.cont = torch.zeros(6, dtype=torch.bool)
self.lo, self.hi = torch.full((6,), -10.0), torch.full((6,), 10.0)
def gaps(self, q, cap):
return {'gm': q[:, :1].expand(-1, 49).clamp(max=cap)}
traj = torch.zeros(1, 3, 6)
traj[0, :, 0] = torch.tensor([1.0, 1.0, -1.0])
per = {'col': torch.tensor([1.0]), 'first_hit': torch.tensor([0.04]),
'dls_it': torch.tensor([-1]), 'traj': traj}
with tempfile.TemporaryDirectory() as folder:
trace = Path(folder) / 'trace.pt'
torch.save({'tasks': [self.tasks[0]], 'batches': [({}, per, 1)]}, trace)
with patch('ml.eval.diag_collision.Kin', PlaneKin), contextlib.redirect_stdout(io.StringIO()):
result = replay_first_hits(trace)
self.assertEqual(result['reported_collision_tasks'], 1)
self.assertEqual(result['replayed_first_hits32'], 1)
self.assertEqual(result['replayed_first_hits64'], 1)
self.assertEqual(result['rows'][0]['gap64_mm'], [0.5, 0.5, 0.0, -1.0])
self.assertEqual(result['rows'][0]['dense_gap64_min_mm'], -1.0)
class DlsSafetyTest(unittest.TestCase):
def test_rejected_refinement_retains_safe_candidate(self):
class ObstacleKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
return {'gm': torch.where(q[:, :1] > 0.3, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
task = {'q0': [0] * 6, 'T': [2, 0, 0]}
traj = torch.zeros(1, 2, 6)
traj[0, 1, 0] = 0.1
ref = torch.zeros(1, 1, 6)
ref[0, 0, 0] = 1.0
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 2.0), torch.zeros(q.shape[0])),
'dls_steps': lambda *args, **kwargs: ref,
}
results = []
with patch.multiple('ml.sim.closed_loop', **patches):
for gate in (0, 1):
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.02, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(traj), replan='empty', commit_n=1,
safety_partial=1, dls_safety=gate)
results.append(per)
self.assertEqual(results[0]['col'].item(), 1.0)
self.assertEqual(results[0]['dls_it'].item(), 0)
self.assertEqual(results[1]['col'].item(), 0.0)
self.assertEqual(results[1]['dls_it'].item(), -1)
self.assertAlmostEqual(results[1]['traj'][0, 1, 0].item(), 0.1)
def test_feedback_is_reduced_only_after_full_rejection(self):
class ObstacleKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
return {'gm': torch.where(q[:, :1] > 0.3, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
traj = torch.zeros(1, 17, 6)
traj[0, :, 0] = -0.2 * torch.arange(17)
ref = torch.zeros(1, 1, 6)
ref[0, 0, 0] = 1.0
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
'dls_steps': lambda *args, **kwargs: ref,
}
results = []
with patch.multiple('ml.sim.closed_loop', **patches):
for gain in (1.0, 0.5):
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.04, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(traj), replan='empty', commit_n=1,
safety_partial=1, fb_gain=0.6, fb_block_gain=gain)
results.append(per)
self.assertEqual(results[0]['brakes'].item(), 2.0)
self.assertEqual(results[1]['brakes'].item(), 1.0)
self.assertEqual(results[1]['traj'][0, 1, 0].item(), 0.0)
self.assertAlmostEqual(results[1]['traj'][0, 2, 0].item(), 0.1)
self.assertEqual(results[1]['col'].item(), 0.0)
if __name__ == '__main__':
unittest.main()