dsh-eval-guard 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/ISSUES.md +239 -0
  2. package/README.md +48 -18
  3. package/assets/neural-cifar/workspace.zip +0 -0
  4. package/assets/neural-cpp/workspace.zip +0 -0
  5. package/assets/neural-fp32/workspace.zip +0 -0
  6. package/assets/neural-pendulum/SPEC.md +43 -35
  7. package/assets/neural-pendulum/TASK.md +139 -33
  8. package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
  9. package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
  10. package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
  11. package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
  12. package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
  13. package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
  14. package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
  15. package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
  16. package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
  17. package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
  18. package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
  19. package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
  20. package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
  21. package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
  22. package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
  23. package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
  24. package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
  25. package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
  26. package/assets/neural-pendulum/sim.cpp +13 -2
  27. package/assets/neural-pendulum/sim_eval +0 -0
  28. package/assets/neural-pendulum/workspace.zip +0 -0
  29. package/eval/README.md +19 -9
  30. package/eval/bomberman.py +5 -3
  31. package/eval/commit0.py +10 -3
  32. package/eval/common.py +169 -5
  33. package/eval/ctf.py +4 -2
  34. package/eval/featurebench.py +12 -8
  35. package/eval/land.py +4 -2
  36. package/eval/legacy/bomberman.py +5 -2
  37. package/eval/legacy/ctf.py +5 -2
  38. package/eval/legacy/featurebench.py +55 -15
  39. package/eval/legacy/land.py +6 -3
  40. package/eval/legacy/swe_bench_pro.py +57 -16
  41. package/eval/legacy/tank.py +2 -2
  42. package/eval/legacy/warehouse.py +4 -4
  43. package/eval/neural-cpp.py +9 -6
  44. package/eval/neural-fp32.py +17 -5
  45. package/eval/swe-bench-pro.py +11 -7
  46. package/eval/tank.py +11 -2
  47. package/eval/warehouse.py +17 -1
  48. package/lib/client/index.js +56 -29
  49. package/lib/client/index.js.map +1 -1
  50. package/lib/index.js +883 -298
  51. package/native/dsh-eval-supervisor +0 -0
  52. package/native/supervisor.c +232 -36
  53. package/neural-cifar-eval/README.md +5 -0
  54. package/neural-cifar-eval/common.py +13 -0
  55. package/neural-cifar-eval/ops_eval.py +17 -5
  56. package/neural-cifar-eval/run_eval.py +42 -32
  57. package/neural-pendulum-eval/README.md +62 -0
  58. package/neural-pendulum-eval/ops_eval.py +603 -0
  59. package/neural-pendulum-eval/run_eval.py +218 -137
  60. package/neural-pendulum-eval/sim_eval +0 -0
  61. package/package.json +7 -11
  62. package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
  63. package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
@@ -8,9 +8,9 @@
8
8
 
9
9
  - 训练时从随机小扰动、评测时从固定初始状态起摆
10
10
  - 保持三根杆全部向上至少 t_tgt = 10.0 秒
11
- - 同时保证神经网络基础设施通过固定头文件契约并通过隐藏测试
11
+ - 同时保证神经网络基础设施通过 build/nn_task 固定 CLI 的数值评测
12
12
 
13
- 开发阶段可以使用 Python/PyTorch 做离线数值对照和实验;最终提交的基础设施和控制器源码必须是纯 C++,不得出现 torch,评测环境不提供 Python/torch。
13
+ 评测侧会用 PyTorch 计算算子参考值,但**工作区/候选运行时不提供 PyTorch 或 numpy**,也没有网络,无法 pip 安装。开发阶段请用 C++,或 Python 标准库/独立 double 实现做数值对照;最终提交的基础设施和控制器源码必须是纯 C++,不得出现 torch。
14
14
 
15
15
  ## 环境
16
16
 
@@ -47,7 +47,7 @@ joint 2:被动
47
47
  joint 3:驱动
48
48
  ~~~
49
49
 
50
- 第一关节和第三关节各有一个归一化力矩输入。
50
+ 第一关节和第三关节各有一个归一化力矩输入。第三关节是杆 2/杆 3 之间的内部电机:它的力矩同时作用在杆 3 和杆 2 上(反作用),不是只作用在杆 3 上的外部力矩;关节 2 没有主动电机力矩。
51
51
 
52
52
  ### 观测
53
53
 
@@ -61,6 +61,7 @@ obs = [a1, a2, a3] (rad)
61
61
  即 3 维观测,单位 rad,不除以 pi。
62
62
 
63
63
  - theta_i 是第 i 根杆的绝对角度
64
+ - theta_i = 0 表示竖直向下,theta_i = pi 表示竖直向上
64
65
  - a_i 是相对竖直向上的角度误差
65
66
  - 不提供角速度
66
67
  - 不提供第三杆末端位置
@@ -100,7 +101,7 @@ t_can = 三根杆同时处于全向上容差内的最长连续时间
100
101
 
101
102
  ~~~
102
103
  t_can >= t_tgt
103
- 且基础设施测试全部通过
104
+ 且神经网络基础设施数值评测全部通过
104
105
  ~~~
105
106
 
106
107
  任务完成,计时停止。
@@ -152,34 +153,140 @@ cmake --build build -j4
152
153
  必须产出:
153
154
 
154
155
  ~~~
155
- build/libnn.a
156
+ build/nn_task
156
157
  build/control
157
158
  ~~~
158
159
 
159
- ### 1. 基础设施入口
160
+ 工作区模板 CMakeLists.txt 默认从 `src/core/*.cpp` 构建共享静态库,从 `src/nn_task.cpp` 和 `src/control.cpp` 构建两个可执行文件。可以按需调整目录结构和 CMakeLists.txt,但必须从零构建出上面两个二进制。
160
161
 
161
- 基础设施实现放在 `src/nn/*.cpp`,由 CMake 构建为 `build/libnn.a`;公开头文件 `include/nn/nn.hpp` 是固定 API 契约,不得修改声明或语义。
162
+ ### 1. 神经网络基础设施入口 build/nn_task
162
163
 
163
- 评测侧会独立编译隐藏测试并链接 `build/libnn.a`。
164
+ 基础设施通过固定 CLI 暴露,评测会逐操作调用。所有输入/输出文件都是连续 row-major float32,所有数值输出也必须写为 float32。
164
165
 
165
- 必须包含的基础设施:
166
+ #### 逐元素
166
167
 
167
- - fp32 tensor,形状检查与广播
168
- - elementwise add/sub/mul/div
169
- - matmul / linear
170
- - relu / softmax
171
- - layernorm
172
- - conv2d,支持 stride/padding/dilation/groups
173
- - MSE
174
- - matmul/linear/relu/layernorm/conv2d/MSE 反向
175
- - SGD momentum
176
- - AdamW
177
- - 模型保存/加载
178
- - 线程池
168
+ ~~~bash
169
+ build/nn_task elementwise --op add|sub|mul|div --shape 64,256 --a A --b B --out O
170
+ ~~~
171
+
172
+ 支持广播:从尾部对齐,维度为 1 可广播。
173
+
174
+ #### 矩阵乘法
175
+
176
+ ~~~bash
177
+ build/nn_task matmul --a-shape M,K --b-shape K,N --a A --b B --out O
178
+ ~~~
179
+
180
+ 只要求 2D 输入。
181
+
182
+ #### Linear
183
+
184
+ ~~~bash
185
+ build/nn_task linear --x-shape B,IN --w-shape OUT,IN --b-shape OUT --x X --w W --bias B --out O
186
+ ~~~
187
+
188
+ 计算 `O = X @ W^T + bias`。
189
+
190
+ #### 激活
191
+
192
+ ~~~bash
193
+ build/nn_task relu --shape N --x X --out O
194
+ build/nn_task softmax --shape B,C --x X --out O
195
+ ~~~
196
+
197
+ Softmax 沿最后一维。
198
+
199
+ #### LayerNorm
200
+
201
+ ~~~bash
202
+ build/nn_task layernorm --shape B,C --x X --gamma G --beta B --eps 1e-5 --out O
203
+ ~~~
204
+
205
+ 对最后一维归一化;gamma/beta 形状为 `C`。
206
+
207
+ #### MSE
208
+
209
+ ~~~bash
210
+ build/nn_task mse --pred-shape S --target-shape S --pred P --target T --out O
211
+ ~~~
212
+
213
+ 输出 1 个 float32,为所有元素平均平方误差。
214
+
215
+ #### Conv2d
216
+
217
+ ~~~bash
218
+ build/nn_task conv2d --x-shape N,C,H,W --w-shape O,Cg,KH,KW --b-shape O --x X --w W --bias B --stride SH,SW --padding PH,PW --dilation DH,DW --groups G --out OUT
219
+ ~~~
220
+
221
+ 标准 NCHW 卷积,`Cg = C / groups`。
222
+
223
+ #### 反向传播
224
+
225
+ ~~~bash
226
+ build/nn_task matmul-backward --a-shape M,K --b-shape K,N --a A --b B --grad G --da DA --db DB
227
+ build/nn_task linear-backward --x-shape B,IN --w-shape OUT,IN --b-shape OUT --x X --w W --grad G --dx DX --dw DW --db DB
228
+ build/nn_task relu-backward --shape N --x X --grad G --dx DX
229
+ build/nn_task layernorm-backward --shape B,C --x X --gamma G --grad G --eps 1e-5 --dx DX --dgamma DG
230
+ build/nn_task mse-backward --pred-shape S --target-shape S --pred P --target T --grad G --dp DP --dtarget DT
231
+ build/nn_task conv2d-backward --x-shape N,C,H,W --w-shape O,Cg,KH,KW --b-shape O --x X --w W --grad G --stride SH,SW --padding PH,PW --dilation DH,DW --groups G --dx DX --dw DW --db DB
232
+ ~~~
233
+
234
+ 反向传播定义为:上游梯度 `G` 对输入、权重、bias 的梯度;`G` 与 forward 输出同形状。
235
+
236
+ #### 优化器
237
+
238
+ ~~~bash
239
+ build/nn_task adamw --param P --grad G --m M --v V --lr LR --beta1 B1 --beta2 B2 --eps EPS --weight-decay WD --step T --out-param P2 --out-m M2 --out-v V2
240
+ build/nn_task sgd-momentum --param P --grad G --momentum-buffer MB --lr LR --momentum MU --out-param P2 --out-buffer MB2
241
+ ~~~
242
+
243
+ AdamW 使用 decoupled weight decay:
244
+
245
+ ~~~text
246
+ m_t = beta1*m + (1-beta1)*g
247
+ v_t = beta2*v + (1-beta2)*g*g
248
+ m_hat = m_t / (1-beta1^t)
249
+ v_hat = v_t / (1-beta2^t)
250
+ param = param*(1-lr*wd) - lr*m_hat/(sqrt(v_hat)+eps)
251
+ ~~~
252
+
253
+ SGD momentum:
254
+
255
+ ~~~text
256
+ buffer = momentum*buffer + grad
257
+ param = param - lr*buffer
258
+ ~~~
259
+
260
+ #### 数值评测方式
261
+
262
+ 评测侧在独立进程中生成数据,并用 PyTorch 在相同输入/参数上计算参考值,逐操作比较。工作区不会收到期望输出张量。
263
+
264
+ - 每个操作使用 3 组独立随机数据(固定种子),形状固定
265
+ - 误差指标为加权平均相对误差:
266
+
267
+ ~~~
268
+ mre = sum(|candidate - reference|) / sum(|reference|)
269
+ ~~~
270
+
271
+ - 每个操作有独立阈值,单位与 fp32 精度匹配:
272
+
273
+ | 操作 | 加权平均相对误差阈值 |
274
+ | --- | --- |
275
+ | elementwise.add / sub / mul / div | 1e-6 |
276
+ | relu | 1e-6 |
277
+ | mse | 1e-6 |
278
+ | sgd-momentum | 1e-6 |
279
+ | matmul / linear | 1e-5 |
280
+ | softmax / layernorm | 1e-5 |
281
+ | adamw | 1e-5 |
282
+ | conv2d | 1e-4 |
283
+ | matmul-backward / linear-backward / relu-backward / layernorm-backward / mse-backward / conv2d-backward | 1e-4 |
179
284
 
180
- 占位符或空实现会被判为基础设施错误。
285
+ - 任一操作不可执行、崩溃、超时(单次调用 60 秒)、输出文件缺失或元素数不符,或 mre 超过阈值,都会判为基础设施失败(stage = infra)
286
+ - 速度(GFLOP/s)会同时报告,但速度不参与通过判定
287
+ - 不做完整训练,不要求模型保存/加载格式;控制器需要的权重可以放在源码目录或控制器自行打包
181
288
 
182
- 隐藏测试由评测侧注入,使用固定种子和独立数据,不复制到工作区。
289
+ 评测数据在测试进程内生成,不随工作区提供。不要伪造输出:评测直接执行你的二进制并检查它写出的文件。
183
290
 
184
291
  ### 2. 控制器入口
185
292
 
@@ -222,7 +329,7 @@ eval_test
222
329
 
223
330
  ### eval_test
224
331
 
225
- 由宿主对最新提交快照进行构建和隐藏仿真评估,返回:
332
+ 由宿主对最新提交快照进行构建、算子数值评测和隐藏仿真评估,返回:
226
333
 
227
334
  ~~~text
228
335
  stage
@@ -248,8 +355,8 @@ completed
248
355
  阶段含义:
249
356
 
250
357
  - no_submission:还没有提交快照
251
- - compile:CMake 构建 `libnn.a` / `control` 失败
252
- - infra:隐藏基础设施测试编译、链接或运行失败
358
+ - compile:CMake 构建失败,或没有产出 build/nn_task / build/control
359
+ - infra:build/nn_task 算子数值评测失败(不可执行、崩溃、超时、输出缺失或误差超阈值)
253
360
  - controller:控制器启动、协议、崩溃、超时或输出非法
254
361
  - simulation:隐藏仿真推进异常、NaN 或超时
255
362
  - scoring:测试有效,但 `t_can < 10.0`
@@ -278,10 +385,9 @@ t_can >= 10.0
278
385
 
279
386
  - 只允许 C++17 + CMake + C++ 标准库
280
387
  - 仅 CPU,fp32
281
- - 提交源码中不得出现 torch
282
- - 评测环境无 Python/torch
388
+ - 提交源码中不得出现 torch;候选运行时不提供 Python/torch(评测侧会用 PyTorch 计算数值参考值)
283
389
  - 无网络
284
- - 不得修改仿真器或公开头文件 `include/nn/nn.hpp`
390
+ - 不得修改或替换 tools/sim;它只用于训练和自测
285
391
  - 不得提交编译产物
286
392
  - 赛后人工审核:控制器必须是神经网络,且必须实际调用你实现的神经网络基础设施;手写控制律、查表、固定轨迹或外部依赖会被判无效
287
393
 
@@ -361,9 +467,9 @@ T_done 是第一次返回 completed 的测试请求时间。
361
467
 
362
468
  ## 建议工作顺序
363
469
 
364
- 1. 阅读 `include/nn/nn.hpp` 契约与 tools/sim 协议
365
- 2. 先实现最小可用的 tensor/算子/反向/优化器
366
- 3. 用自测和 `include/nn/nn.hpp` 契约检查基础设施
367
- 4. 用 tools/sim 采集数据并训练控制器
470
+ 1. 阅读 build/nn_task 的 CLI 协议与 tools/sim 协议
471
+ 2. 先实现最小可用的 tensor/算子/反向/优化器/线程池,并让 build/nn_task 跑通
472
+ 3. 用独立 double 标量参考、有限差分梯度检查逐算子核对误差,边界形状/广播/groups/stride/dilation 都要覆盖(工作区没有 PyTorch/numpy)
473
+ 4. 用 tools/sim 采集数据并训练控制器,导出权重供 build/control 使用
368
474
  5. 反复提交和测试,优先把 t_can 提升到 10 秒
369
475
  6. 完成后立即总结
@@ -0,0 +1,60 @@
1
+ # Neural-Pendulum RL feasibility harness (dev-only reference)
2
+
3
+ This directory is a **development/reference** copy of the reinforcement-learning
4
+ environment used to confirm that the corrected Neural-Pendulum dynamics are
5
+ learnable. It is **not** part of the candidate runtime, is not copied into the
6
+ task workspace, and is not required by the evaluator.
7
+
8
+ ## Fixed physics
9
+
10
+ The Python simulator here uses the same physical model as
11
+ `assets/neural-pendulum/sim.cpp` / `sim_eval`:
12
+
13
+ - three-link planar pendulum, 100 Hz ZOH, RK4 with 4 substeps;
14
+ - physical joint-motor mapping in absolute-angle coordinates:
15
+ `(tau1, -tau3, tau3)` (joint 3 is the internal motor between links 2 and 3,
16
+ joint 2 is passive);
17
+ - corrected Coriolis/centrifugal term
18
+ `C_i = sum_j a_ij sin(theta_i-theta_j) omega_j^2`;
19
+ - 30 s episodes, success tolerance 0.05 rad, t_tgt = 10 s.
20
+
21
+ The Python simulator was cross-checked against `../../sim_eval` on the fixed
22
+ initial state and identical action sequences; maximum observation error was
23
+ about 5e-6 (simulator output has 6 significant digits).
24
+
25
+ ## What was run
26
+
27
+ 15 formal runs (PPO/SAC, observation ablations, reward ablations, fixed vs
28
+ random init; 3 PPO seeds) in the corrected environment. Full report:
29
+ `RESULTS.md`; machine table: `results/table.md`; aggregate:
30
+ `results/summary.json`; selected curves: `results/curves_selected.png`.
31
+
32
+ Core result:
33
+
34
+ - PPO full 36D (cos/sin + manual-difference da/5, H=4), base reward, random
35
+ init: first success 11.33M env steps / 16 min; best t_can 26.85 s.
36
+ - SAC full 36D, base reward, random init: first success 0.70M env steps;
37
+ best t_can 28.75 s.
38
+ - Angle-only (no da) and H=1 observations fail; sparse/energy rewards fail in
39
+ this budget; PPO needs random init, SAC can also learn from fixed init.
40
+ - A non-RL pole-placement reference with 0.5 low-pass holds 29.99 s from the
41
+ fixed upright offset and succeeds on 32/32 random upright offsets, proving
42
+ the hold is physically feasible.
43
+
44
+ ## Run
45
+
46
+ Requires Python 3 with CPU PyTorch on the development host. This is **not**
47
+ available to the candidate sandbox and is not a submission dependency.
48
+
49
+ ```bash
50
+ cd assets/neural-pendulum/rl-feasibility
51
+ TORCH_THREADS=4 python3 test_contract.py
52
+ TORCH_THREADS=4 python3 train_ppo.py --exp local_ppo --init rand --obs full \
53
+ --reward base --envs 64 --minutes 30 --tb-root tb --ckpt-dir checkpoints --out results
54
+ tensorboard --logdir tb
55
+ ```
56
+
57
+ TensorBoard event files are written live during training. The experiment
58
+ matrix and per-run JSON/TB/log files from the full sweep are kept outside the
59
+ npm package; only the source, report, summary, table and selected curve are
60
+ bundled here to keep the plugin package small.
@@ -0,0 +1,370 @@
1
+ # RESULTS — fixed-physics 3-link pendulum RL feasibility
2
+
3
+ Work dir: `feasibility/neural-pendulum-fixedenv`.
4
+ Started 2026-10-23 01:13 CST; experiment matrix completed 06:35 CST.
5
+ All numbers below are new runs in the corrected environment; no old
6
+ `(tau1, 0, tau3)` result is reused.
7
+
8
+ ## TL;DR
9
+
10
+ * **PPO and SAC both learn the corrected dynamics from the mandated random
11
+ near-down resets and reach t_can >= 10 s at the fixed evaluation state.**
12
+ * PPO (64 env, full 36D, base reward): first success at **11.33M env steps /
13
+ 16.0 min**; best **26.85 s**; final 26.61 s; best-checkpoint robustness
14
+ **8/8**.
15
+ * SAC (32 env, full 36D, base reward): first success at **0.70M env steps /
16
+ 24.7 min**; best **28.75 s**; final 28.21 s; best-checkpoint robustness
17
+ **6/8** (final 8/8).
18
+ * **The learnability boundary is set by observation information, not by the
19
+ algorithm alone.**
20
+ * `full` H=4 (36D: cos/sin + da/5) works for both PPO and SAC.
21
+ * `angle` H=4 (24D, no da): PPO best 0.97 s, SAC best 0.04 s — fails.
22
+ * `full_h1` (9D) and `angle_h1` (6D): fail completely (best <= 0.06 s);
23
+ even a 50% upright curriculum does not rescue H=1.
24
+ * `angle_h8` (48D): best 1.31 s — longer history helps a little but does
25
+ not replace `da/5`.
26
+ * **Reward boundary:** base works; sparse and strong energy shaping fail.
27
+ * `sparse`: 19.12M steps, best t_can 0, policy converges to tiny actions.
28
+ * `energy`: two designs fail; energy chasing with manual `da` has a
29
+ degenerate high-kinetic-energy spin optimum.
30
+ * **Init boundary is algorithm-dependent.** Random init is required for PPO
31
+ (fixed-init PPO: best 1.19 s, robust 0/8). SAC fixed-init *does* succeed
32
+ (best 28.86 s, robust 8/8) because its replay covers the state space; PPO's
33
+ on-policy batch of 64 identical states does not.
34
+ * Three PPO seeds all reach >26 s at their best checkpoint (26.85 / 27.17 /
35
+ 28.46 s) but the final checkpoint is unreliable (seed 2 final 0.00 s).
36
+ Use `_best.pt`, not the last model.
37
+ * A non-RL pole-placement stabiliser with a 0.5 low-pass torque filter holds
38
+ **29.99 s** from the fixed upright offset and succeeds on 32/32 random
39
+ upright offsets. The hold is physically feasible with the corrected
40
+ mapping; it is not an RL result (privileged omega).
41
+
42
+ ## 1. Environment and contract
43
+
44
+ * Simulator returns angles only: `reset(theta,omega)->theta`,
45
+ `step(u)->theta`. No reward, done, info, omega or tip is exposed.
46
+ * Corrected physical torque mapping:
47
+ `generalized_torque = (tau1, -tau3, tau3)` (joint 3 is the internal motor
48
+ between links 2 and 3; joint 2 is passive). `sim.generalized_torque` and
49
+ `sim.make_torch_consts` both implement it. Do not revert to
50
+ `(tau1, 0, tau3)`.
51
+ * 100 Hz ZOH, RK4 with 4 substeps, 30 s episodes (3000 steps),
52
+ tau_max = 18 N m, damping 0.01, success tolerance 0.05 rad, t_tgt = 10 s.
53
+ * `test_contract.py` passes; the Python sim was cross-validated against
54
+ `assets/neural-pendulum/sim_eval` by the Lead (~5e-6).
55
+ * Training reset (random arm): theta_i ~ U(-0.05,0.05),
56
+ omega_i ~ U(-0.02,0.02), independent, no fixed bias.
57
+ Fixed arm: theta = [0.013,-0.008,0.021], omega = 0.
58
+ Curriculum arm: 50% near down, 50% theta = pi + U(-0.05,0.05).
59
+ * Evaluation: fixed theta = [0.013,-0.008,0.021], omega = 0, deterministic
60
+ actions, full 3000 steps. t_can is the longest continuous interval with
61
+ max_i |wrap(theta_i - pi)| < 0.05 rad. Eval always uses the **same** obs
62
+ mode and reward preset as training; no reward switch at eval.
63
+
64
+ ## 2. Code changes in this directory
65
+
66
+ 1. `obs.py`: switchable modes
67
+ `full` 36D, `angle` 24D, `full_h1` 9D, `angle_h1` 6D,
68
+ `full_h2` 18D, `angle_h2` 12D, plus `angle_h8` 48D.
69
+ `da` is always computed internally because the reward uses it;
70
+ `diff=False` only drops it from the policy input.
71
+ 2. `task.py`: reward presets `base`, `sparse`, `energy` (exact weights
72
+ below), bounded energy error, and `--init mix` curriculum support.
73
+ 3. `train_ppo.py` / `train_sac.py`: `--obs`, `--reward`, `--init`,
74
+ `--up-frac`; eval uses the same mode/reward; live TB flush every
75
+ iteration/log tick; 8 extra deterministic rollouts from the training
76
+ reset distribution for final and best checkpoints.
77
+ 4. `evaluate.py`: obs-mode aware, plus `evaluate_starts`.
78
+ 5. `reference_controller.py`: non-RL local stabilisability proof.
79
+ 6. `finalize_run.py`: rebuild a results JSON from checkpoints + log when a
80
+ trainer crashes after saving checkpoints (used once for PPO seed 0).
81
+ 7. `summarize.py`, `plot_selected.py`: aggregate table/JSON/curves.
82
+
83
+ ## 3. Observation modes
84
+
85
+ | --obs | frame | H | dim | contents |
86
+ |---|---|---|---|---|
87
+ | full | [cos a_i, sin a_i, da_i/5] | 4 | 36 | baseline |
88
+ | angle | [cos a_i, sin a_i] | 4 | 24 | no explicit velocity |
89
+ | full_h1 | [cos a_i, sin a_i, da_i/5] | 1 | 9 | single frame |
90
+ | angle_h1 | [cos a_i, sin a_i] | 1 | 6 | single frame, no velocity |
91
+ | full_h2 | [cos a_i, sin a_i, da_i/5] | 2 | 18 | optional |
92
+ | angle_h2 | [cos a_i, sin a_i] | 2 | 12 | optional |
93
+ | angle_h8 | [cos a_i, sin a_i] | 8 | 48 | longer history follow-up |
94
+
95
+ `a_i = wrap_to_pi(theta_i - pi)`, `da_i = wrap_to_pi(a_i(t)-a_i(t-1))/dt`,
96
+ `da(0)=0`. Theta is in radians, never divided by pi. History is oldest
97
+ first, reset repeats the first frame. The eval builder is the same class and
98
+ mode as training.
99
+
100
+ ## 4. Reward presets (exact)
101
+
102
+ ```
103
+ r = exp * exp(-2 s2)
104
+ + sharp * exp(-50 s2)
105
+ - ang * s2
106
+ - vel * sum da^2
107
+ - act * sum u^2
108
+ + up * 1[max_i |a_i| < 0.15]
109
+ + tip * (tip_y + L)/(2L)
110
+ - energy * |E - E_up| / (E_up - E_down)
111
+ ```
112
+
113
+ All terms are trainer-side functions of theta, the manual difference da and
114
+ the action u; no simulator omega is read. `E` uses da as velocity. The
115
+ energy error is clamped at 3 to keep the reward bounded at high spin.
116
+
117
+ ```json
118
+ base = {"exp":1.0,"sharp":2.0,"ang":0.05,"vel":0.001,"act":0.01,"up":3.0,"tip":0.3,"energy":0.01}
119
+ sparse = {"exp":0.2,"sharp":0.0,"ang":0.0,"vel":0.001,"act":0.01,"up":3.0,"tip":0.0,"energy":0.0}
120
+ energy = {"exp":1.0,"sharp":2.0,"ang":0.05,"vel":0.0,"act":0.005,"up":3.0,"tip":0.0,"energy":0.5}
121
+ ```
122
+
123
+ `sparse`: at the near-down reset the only dense term is ~1e-26, so it is an
124
+ intentionally hard exploration test. `energy`: base angle terms + strong
125
+ energy shaping + no velocity penalty; an earlier energy-dominant design
126
+ (`exp=.2, sharp=.5, ang=.01, vel=.0002`) is archived under
127
+ `failed/energy_unbounded/`.
128
+
129
+ ## 5. Training protocol
130
+
131
+ PPO: 2x128 tanh actor/critic, 64 envs, horizon 600, 8 epochs,
132
+ minibatch 2048, lr 3e-4 linearly decayed to 20%, gamma .99, lambda .95,
133
+ clip .2, entropy .003, vf .5, target KL .03, grad clip .5.
134
+
135
+ SAC: 2x256 tanh squashed-Gaussian actor, shared-trunk twin Q, 32 envs,
136
+ batch 256, replay 300k, auto-alpha (target entropy -2), gamma .99,
137
+ tau .005, lr 3e-4, UTD 0.25 (8 gradient updates per 32-env vector step).
138
+
139
+ Both: CPU PyTorch, 4 threads per process, at most 2 training processes
140
+ concurrently. Eval every 5 PPO iterations (192k steps) or every 20k SAC
141
+ steps. Best checkpoint by eval t_can (ties by return). Final checkpoint at
142
+ the time budget. TensorBoard flushed during training (not only at the end).
143
+ At the end, 8 deterministic rollouts from the training distribution are run
144
+ for the final and best checkpoints.
145
+
146
+ ## 6. Complete experiment matrix (fixed-initial deterministic eval)
147
+
148
+ `best trob` = success rate / mean t_can of the best checkpoint on the 8
149
+ training-distribution starts. `first succ` = first eval snapshot with
150
+ t_can >= 10 s.
151
+
152
+ | exp | algo | obs | reward | init | env steps | wall | best t_can | final t_can | ever_up | best trob | first succ |
153
+ |---|---|---|---|---|---|---|---|---|---|---|---|
154
+ | ppo_full_base_rand | PPO | full 36 | base | rand | 35.98M | 45.1 min | **26.85 s** | 26.61 s | yes | 8/8, 26.86 s | **11.33M** |
155
+ | ppo_full_base_rand_s1 | PPO | full 36 | base | rand | 29.03M | 40.8 min | **27.17 s** | 0.23 s | yes | 8/8, 26.78 s | 13.63M |
156
+ | ppo_full_base_rand_s2 | PPO | full 36 | base | rand | 34.21M | 40.3 min | **28.46 s** | 0.00 s | no | 8/8, 28.03 s | 14.40M |
157
+ | sac_full_base_rand | SAC | full 36 | base | rand | 1.42M | 50.6 min | **28.75 s** | 28.21 s | yes | 6/8, 20.89 s | **0.70M** |
158
+ | ppo_angle_base_rand | PPO | angle 24 | base | rand | 31.76M | 40.6 min | 0.97 s | 0.04 s | yes | 0/8 | - |
159
+ | sac_angle_base_rand | SAC | angle 24 | base | rand | 1.42M | 50.6 min | 0.04 s | 0.00 s | no | 0/8 | - |
160
+ | ppo_full_h1_rand | PPO | full_h1 9 | base | rand | 25.42M | 35.3 min | 0.06 s | 0.00 s | no | 0/8 | - |
161
+ | ppo_angle_h1_rand | PPO | angle_h1 6 | base | rand | 20.97M | 30.4 min | 0.00 s | 0.00 s | no | 0/8 | - |
162
+ | ppo_angle_h8_rand | PPO | angle_h8 48 | base | rand | 30.11M | 40.4 min | 1.31 s | 0.05 s | yes | 0/8 | - |
163
+ | ppo_full_h1_mix | PPO | full_h1 9 | base | mix 50% up | 21.39M | 30.5 min | 0.02 s | 0.00 s | no | 0/8 | - |
164
+ | ppo_angle_mix | PPO | angle 24 | base | mix 50% up | 23.50M | 30.5 min | 0.25 s | 0.00 s | yes | 0/8 | - |
165
+ | ppo_full_sparse_rand | PPO | full 36 | sparse | rand | 19.12M | 30.5 min | 0.00 s | 0.00 s | no | 0/8 | - |
166
+ | ppo_full_energy_rand | PPO | full 36 | energy | rand | 24.50M | 35.5 min | 0.00 s | 0.00 s | no | 0/8 | - |
167
+ | ppo_full_base_fixed | PPO | full 36 | base | fixed | 21.93M | 30.5 min | 1.19 s | 0.00 s | no | 0/8 | - |
168
+ | sac_full_base_fixed | SAC | full 36 | base | fixed | 1.41M | 50.4 min | **28.86 s** | 27.62 s | yes | 8/8, 23.45 s | **0.52M** |
169
+
170
+ Raw machine-readable table: `results/table.md`; per-run JSON:
171
+ `results/<exp>.json`; plots: `results/curves_selected.png`.
172
+
173
+ ## 7. Results by question
174
+
175
+ ### Q1. Are PPO and SAC learnable? Steps/time to t_can >= 10 s
176
+
177
+ **Yes, both, with full 36D observations and base reward.**
178
+
179
+ | algo | envs | first success | wall to first success | steps/s | budget | best | final | best-checkpoint robustness |
180
+ |---|---|---|---|---|---|---|---|---|
181
+ | PPO | 64 | 11.33M | 16.0 min | ~13.3k | 45 min / 35.98M | 26.85 s | 26.61 s | 8/8 (mean 26.86 s) |
182
+ | SAC | 32 | 0.70M | 24.7 min | ~470 | 50 min / 1.42M | 28.75 s | 28.21 s | 6/8 (mean 20.89 s) |
183
+
184
+ The first successful snapshots were 26.65 s (PPO at 11.33M) and 16.74 s
185
+ (SAC at 0.70M). SAC needs ~16x fewer environment steps but ~28x more
186
+ wall-clock per step, so the time to first success is comparable (16 vs
187
+ 25 min). PPO is more stable
188
+ at the final snapshot; SAC's final policy is also strong (8/8, 28.21 s).
189
+ PPO eval t_can is intermittent during learning (0 <-> 26 s between successive
190
+ 192k-step evals), so a single eval snapshot is not a reliable success
191
+ criterion; both `_best.pt` and the success-snapshot count are reported.
192
+
193
+ ### Q2. Angle-only vs full
194
+
195
+ **Removing the explicit da/5 channel is a hard failure in this budget.**
196
+
197
+ | exp | algo | obs | best t_can | final t_can | best robust |
198
+ |---|---|---|---|---|---|
199
+ | ppo_full_base_rand | PPO | full 36 | **26.85 s** | 26.61 s | 8/8 |
200
+ | ppo_angle_base_rand | PPO | angle 24 | 0.97 s | 0.04 s | 0/8 |
201
+ | sac_full_base_rand | SAC | full 36 | **28.75 s** | 28.21 s | 6/8 |
202
+ | sac_angle_base_rand | SAC | angle 24 | 0.04 s | 0.00 s | 0/8 |
203
+
204
+ PPO angle-only can swing up (ever_up=1, first up ~1.2 s) but cannot hold;
205
+ its best up_frac is 3.8%. SAC angle-only only briefly touches upright
206
+ (best t_can 0.04 s, a single 0.04 s window at ~9.23 s; final ever_up 0) and
207
+ its final return is negative. H=4 cos/sin history does not substitute for
208
+ the manually differentiated velocity. This is a budget
209
+ statement, not an information-theoretic impossibility, but 40-50 min and
210
+ 20-32M steps is a generous budget in this study.
211
+
212
+ ### Q3. H=4 -> H=1 and reduced observations
213
+
214
+ **H=1 destroys learnability even with explicit da; longer history only
215
+ partially substitutes for da.**
216
+
217
+ | exp | obs | dim | best t_can | final t_can | ever_up | best robust |
218
+ |---|---|---|---|---|---|---|
219
+ | ppo_full_base_rand | full H=4 | 36 | **26.85 s** | 26.61 s | yes | 8/8 |
220
+ | ppo_full_h1_rand | full H=1 | 9 | 0.06 s | 0.00 s | no | 0/8 |
221
+ | ppo_angle_base_rand | angle H=4 | 24 | 0.97 s | 0.04 s | yes | 0/8 |
222
+ | ppo_angle_h8_rand | angle H=8 | 48 | 1.31 s | 0.05 s | yes | 0/8 |
223
+ | ppo_angle_h1_rand | angle H=1 | 6 | 0.00 s | 0.00 s | no | 0/8 |
224
+ | ppo_full_h1_mix | full H=1 + 50% upright | 9 | 0.02 s | 0.00 s | no | 0/8 |
225
+
226
+ The 9D full_h1 observation has the instantaneous full state (cos/sin+da/5)
227
+ but no temporal window. It never reaches upright in 25.4M steps. A
228
+ curriculum with half the resets already near upright also fails (21.4M
229
+ steps), so the H=1 failure is representational (no phase/acceleration
230
+ history), not only exploration. Angle H=8 improves the best t_can from 0.97
231
+ to 1.31 s but is still far from success; explicit da + H=4 is the only
232
+ working combination.
233
+
234
+ ### Q4. Reward preset
235
+
236
+ | exp | reward | env steps | best t_can | final t_can | ever_up | note |
237
+ |---|---|---|---|---|---|---|
238
+ | ppo_full_base_rand | base | 35.98M | **26.85 s** | 26.61 s | yes | works |
239
+ | ppo_full_sparse_rand | sparse | 19.12M | 0.00 s | 0.00 s | no | policy -> tiny actions |
240
+ | ppo_full_energy_rand | energy v2 | 24.50M | 0.00 s | 0.00 s | no | spinning optimum |
241
+ | (failed v1) | energy unbounded | 6.7M | 0.00 s | 0.00 s | no | value loss ~5e7 |
242
+
243
+ **base** is the only working reward. **sparse** has no usable gradient from
244
+ the near-down reset: `0.2 exp(-2 s2)` is ~1e-26 at s2~30, and the 0.15 rad
245
+ bonus is never reached by exploration; the policy converges to mean |u|~0.01
246
+ and return -0.39. This is an exploration failure, not a crash.
247
+
248
+ **energy** failed in two designs. v1 used an unbounded energy error and
249
+ blew up the value loss; v2 clamped it and kept the base angle terms but still
250
+ never reached upright in 24.5M steps (0/24.5M, final return -1948). The
251
+ reason is degenerate: total mechanical energy `E = T + V` can be driven to
252
+ `E_up` by spinning fast at the wrong angles, and `T` computed from the
253
+ manual `da` is noisy/large exactly there. A strong energy term rewards that
254
+ spin instead of swing-up. The base reward keeps energy weight at 0.01;
255
+ raising it is not a shortcut.
256
+
257
+ ### Q5. Fixed-init vs random-init (algorithm-dependent)
258
+
259
+ | exp | algo | init | env steps | best t_can | final t_can | best-checkpoint robustness |
260
+ |---|---|---|---|---|---|---|
261
+ | ppo_full_base_rand | PPO | random | 35.98M | **26.85 s** | 26.61 s | 8/8 (26.86 s) |
262
+ | ppo_full_base_fixed | PPO | fixed | 21.93M | **1.19 s** | 0.00 s | 0/8 (mean return -99,380) |
263
+ | sac_full_base_rand | SAC | random | 1.42M | **28.75 s** | 28.21 s | 6/8 (20.89 s) |
264
+ | sac_full_base_fixed | SAC | fixed | 1.41M | **28.86 s** | 27.62 s | 8/8 (23.45 s) |
265
+
266
+ PPO fixed-init **fails**, even at the exact evaluation state (best 1.19 s),
267
+ and is catastrophic on the 8 distribution starts (mean return -99k). PPO's
268
+ on-policy batch contains 64 copies of one state, so it never sees the
269
+ balance-recovery states it needs.
270
+
271
+ SAC fixed-init **succeeds** (best 28.86 s, final 27.62 s, 8/8 on the
272
+ distribution starts, first success at 0.52M). SAC's replay buffer is filled
273
+ with trajectories generated by its stochastic policy from that one start, so
274
+ it does see off-state recovery data. This is the opposite of the old
275
+ `(tau1,0,tau3)` round, where fixed-init SAC failed and fixed-init PPO looked
276
+ better at the fixed state. The corrected internal-motor dynamics changes
277
+ which algorithm can exploit a single start.
278
+
279
+ ### Seed variance (PPO full/base/rand)
280
+
281
+ | seed | env steps | first success | best t_can | final t_can | best-checkpoint robustness |
282
+ |---|---|---|---|---|---|
283
+ | 0 | 35.98M | 11.33M | 26.85 s | 26.61 s | 8/8 (26.86 s) |
284
+ | 1 | 29.03M | 13.63M | 27.17 s | 0.23 s | 8/8 (26.78 s) |
285
+ | 2 | 34.21M | 14.40M | 28.46 s | 0.00 s | 8/8 (28.03 s) |
286
+
287
+ All three seeds solve the task at their best checkpoint (8/8 robust), but the
288
+ final checkpoint is unreliable (seed 2 final 0.00 s). First success ranges
289
+ 11.3-14.4M steps (16-20 min). **Always evaluate and ship `_best.pt`; the
290
+ last model is not the policy.**
291
+
292
+ ## 8. Non-RL feasibility reference
293
+
294
+ `reference_controller.py` → `results/reference_controller.json`:
295
+
296
+ * Linearise at upright, pole placement with poles -5..-10, full state
297
+ (theta + simulator omega), torque command low-pass filtered with alpha=0.5.
298
+ * Fixed start theta = pi + [0.013,-0.008,0.021], omega=0:
299
+ **t_can = 29.99 s**, final angle error ~2e-14, saturation 0%.
300
+ * 32 random upright offsets U(-0.05,0.05): **32/32 success**, mean t_can
301
+ 24.79 s.
302
+ * Without the low-pass filter the same controller chatters and holds only
303
+ ~1.1 s; smooth torque commands matter for the 0.05 rad tolerance.
304
+
305
+ This proves the upright hold is physically reachable with
306
+ `(tau1,-tau3,tau3)` and |u|<=1. It uses privileged omega and starts near
307
+ upright, so it is not comparable to the RL numbers; it is an existence check
308
+ and a hint that action smoothness matters.
309
+
310
+ ## 9. Failures / implementation notes (honest list)
311
+
312
+ * PPO seed 0's results JSON was reconstructed by `finalize_run.py`. The
313
+ trainer crashed **after** saving checkpoints and TB, in the post-training
314
+ robustness code (passed `ActorCritic` instead of `ActorCritic.actor` to
315
+ `make_ppo_policy`). Training was unaffected. The bug is fixed in
316
+ `train_ppo.py`; all later PPO runs wrote JSON directly.
317
+ * The first energy preset produced unbounded rewards and a value-loss blow-up
318
+ (~5e7). It was killed at 6.7M steps and archived under
319
+ `failed/energy_unbounded/`. The second energy design is the one in the
320
+ matrix; it fails for a genuine reward-design reason, not a crash.
321
+ * Sparse reward's no-op policy is a real exploration failure, not a logging
322
+ artefact: final mean |u| ~0.01, return -0.39, ever_up 0.
323
+ * Some runs show value-loss spikes (PPO fixed, PPO angle) without NaNs; the
324
+ policies still train/plateau. These are recorded in TB.
325
+ * No simulator physics was changed. The torque mapping was not reverted.
326
+
327
+ ## 10. Artifacts
328
+
329
+ * TensorBoard: `tb/<exp>/` (live event files, all 15 runs).
330
+ * Logs: `logs/<exp>.log` (stdout, contains eval lines and DONE summary).
331
+ * Results: `results/<exp>.json`, aggregate `results/summary.json`,
332
+ `results/table.md`, `results/curves.png`, `results/curves_selected.png`.
333
+ * Checkpoints: `checkpoints/<exp>_{best,last,final}.pt`.
334
+ * Smoke tests: `smoke_archive/`; 6-min probe: `probes/`.
335
+ * Failed/abandoned runs: `failed/energy_unbounded/`, `failed/energy_collapse/`.
336
+ * Reference controller: `results/reference_controller.json`.
337
+
338
+ ## 11. Reproduction
339
+
340
+ ```bash
341
+ cd feasibility/neural-pendulum-fixedenv
342
+ export OMP_NUM_THREADS=4 MKL_NUM_THREADS=4 TORCH_THREADS=4
343
+ python3 test_contract.py
344
+ python3 train_ppo.py --exp ppo_full_base_rand --obs full --reward base \
345
+ --init rand --envs 64 --horizon 600 --minutes 45 --eval-every 5 --seed 0
346
+ python3 train_sac.py --exp sac_full_base_rand --obs full --reward base \
347
+ --init rand --envs 32 --minutes 50 --eval-every 20000 --log-every 10 --seed 0
348
+ python3 summarize.py
349
+ python3 plot_selected.py
350
+ ```
351
+
352
+ ## 12. Recommendations / next steps
353
+
354
+ 1. **For the actual C++ task, the controller should estimate velocity from
355
+ angles and use a 4-frame history (or an explicit recurrent state).** The
356
+ 3D observation is not enough by itself; the agent must reconstruct phase.
357
+ `full` 36D is the strongest configuration in this study.
358
+ 2. **Train from random near-down resets, not one fixed state.** For PPO this
359
+ is the difference between success and failure; even for SAC, random init
360
+ is more robust across the distribution.
361
+ 3. **Use base reward; do not chase sparse or strong energy shaping.** If a
362
+ sparse objective is required, add a curriculum or demonstration warm start;
363
+ pure sparse PPO does nothing in 19M steps.
364
+ 4. **Select checkpoints by eval t_can and ship the best, not the last.**
365
+ Final models collapse in 2 of 3 PPO seeds.
366
+ 5. **If more compute is available**, try (a) a recurrent/GRU policy on the 3D
367
+ angle stream, (b) an action low-pass filter or action-rate penalty, and
368
+ (c) a two-stage curriculum (balance near upright, then swing-up) with
369
+ warm-started PPO. These target the two observed bottlenecks: velocity
370
+ estimation and balance-phase coverage.