dsh-eval-guard 0.7.4 → 0.7.6
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.
- package/ISSUES.md +4 -2
- package/README.md +3 -3
- package/assets/neural-pendulum/SPEC.md +40 -42
- package/assets/neural-pendulum/TASK.md +34 -47
- package/assets/neural-pendulum/sim.cpp +97 -45
- package/assets/neural-pendulum/sim_agent +0 -0
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/lib/index.js +27 -10
- package/neural-pendulum-eval/README.md +4 -3
- package/neural-pendulum-eval/run_eval.py +133 -70
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +1 -1
- package/assets/neural-pendulum/rl-feasibility/README.md +0 -60
- package/assets/neural-pendulum/rl-feasibility/RESULTS.md +0 -370
- package/assets/neural-pendulum/rl-feasibility/evaluate.py +0 -107
- package/assets/neural-pendulum/rl-feasibility/finalize_run.py +0 -168
- package/assets/neural-pendulum/rl-feasibility/models.py +0 -124
- package/assets/neural-pendulum/rl-feasibility/obs.py +0 -205
- package/assets/neural-pendulum/rl-feasibility/plot_selected.py +0 -55
- package/assets/neural-pendulum/rl-feasibility/reference_controller.py +0 -112
- package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
- package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +0 -417
- package/assets/neural-pendulum/rl-feasibility/results/summary.json +0 -1599
- package/assets/neural-pendulum/rl-feasibility/results/table.md +0 -39
- package/assets/neural-pendulum/rl-feasibility/sim.py +0 -324
- package/assets/neural-pendulum/rl-feasibility/summarize.py +0 -175
- package/assets/neural-pendulum/rl-feasibility/task.py +0 -342
- package/assets/neural-pendulum/rl-feasibility/test_contract.py +0 -188
- package/assets/neural-pendulum/rl-feasibility/train_ppo.py +0 -403
- package/assets/neural-pendulum/rl-feasibility/train_sac.py +0 -406
package/lib/index.js
CHANGED
|
@@ -2166,7 +2166,7 @@ const TASKS = [
|
|
|
2166
2166
|
{
|
|
2167
2167
|
id: NEURAL_PENDULUM_TASK,
|
|
2168
2168
|
label: NEURAL_PENDULUM_TASK_LABEL,
|
|
2169
|
-
description: "从零实现纯 C++17 神经网络基础设施(算子数值以评测侧 PyTorch
|
|
2169
|
+
description: "从零实现纯 C++17 神经网络基础设施(算子数值以评测侧 PyTorch 参考校验),用三个关节角度控制预编译三节摆起摆并稳定保持;评测随机隐藏回合,统计成功率。",
|
|
2170
2170
|
timeLimitMinutes: 1440,
|
|
2171
2171
|
timeLimitEnabled: false,
|
|
2172
2172
|
needsSetup: true
|
|
@@ -2482,13 +2482,13 @@ const NEURAL_PENDULUM_DEFINITION = {
|
|
|
2482
2482
|
"",
|
|
2483
2483
|
"请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/nn_task 与 build/control。",
|
|
2484
2484
|
"",
|
|
2485
|
-
"环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。theta=0 表示竖直向下、theta=pi 表示竖直向上;观测 a_i=wrap(theta_i-pi),即三个关节角度(弧度,不除 pi)。环境不返回 reward/loss/cost/done;不提供杆长、质量、惯量、角速度或 tip
|
|
2485
|
+
"环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。theta=0 表示竖直向下、theta=pi 表示竖直向上;观测 a_i=wrap(theta_i-pi),即三个关节角度(弧度,不除 pi)。环境不返回 reward/loss/cost/done;不提供杆长、质量、惯量、角速度或 tip。训练与评测的每个回合都会随机化物理参数、初始状态和观测噪声;评测在 100 个隐藏随机回合上统计成功率,单回合连续全向上保持 10 s 记成功。",
|
|
2486
2486
|
"",
|
|
2487
|
-
"优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad
|
|
2487
|
+
"优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差并保持;评测指标为 100 个隐藏随机回合上的成功率(每回合连续保持 10 s 记成功),成功率 >= 80% 即完成。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
|
|
2488
2488
|
"",
|
|
2489
2489
|
"注意:评测侧用 PyTorch 计算算子参考值,但工作区/候选运行时不提供 PyTorch 或 numpy,也没有网络,无法 pip 安装;开发阶段请用 C++ 或 Python 标准库/独立 double 实现做数值对照。",
|
|
2490
2490
|
"",
|
|
2491
|
-
"评测会从干净副本重新构建提交源码,先运行与 Neural-FP32/Neural-CIFAR 相同的算子数值评测:评测侧用 PyTorch
|
|
2491
|
+
"评测会从干净副本重新构建提交源码,先运行与 Neural-FP32/Neural-CIFAR 相同的算子数值评测:评测侧用 PyTorch 在相同数据上计算参考值,逐操作比较加权平均相对误差并对照阈值,任一算子不可执行、崩溃、超时、输出缺失或误差超阈值即判为基础设施失败;然后在 100 个随机隐藏回合上驱动 build/control,统计成功率(单回合连续全向上保持 10 s 记成功)。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
|
|
2492
2492
|
"使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
|
|
2493
2493
|
"",
|
|
2494
2494
|
"工作方式:先完成最小可构建的基础设施和 control 协议;再持续做控制/训练实验并记录可复现结果;最终保证工作区从零构建、build/nn_task 算子数值评测通过、control 可被评测驱动,并留下 NOTES.md/RESULTS.md。",
|
|
@@ -2680,15 +2680,17 @@ var TaskService = class TaskService {
|
|
|
2680
2680
|
if (text.includes("controller") || text.includes("torque")) return "controller";
|
|
2681
2681
|
return "simulation";
|
|
2682
2682
|
}
|
|
2683
|
-
/**
|
|
2684
|
-
static
|
|
2683
|
+
/** Evaluation success-rate target used by the neural-pendulum scoring integral. */
|
|
2684
|
+
static FINAL_SUCCESS_TARGET = .8;
|
|
2685
2685
|
/** One test event's instantaneous score, mirroring the task specification. */
|
|
2686
2686
|
static eventScore(event) {
|
|
2687
2687
|
const stage = String(event["stage"] ?? "");
|
|
2688
2688
|
if (stage !== "scoring" && stage !== "completed" && event["valid"] !== true) return -1;
|
|
2689
|
+
const rate = Number(event["success_rate"]);
|
|
2690
|
+
if (Number.isFinite(rate)) return (Math.min(Math.max(rate, 0), TaskService.FINAL_SUCCESS_TARGET) - TaskService.FINAL_SUCCESS_TARGET) / TaskService.FINAL_SUCCESS_TARGET;
|
|
2689
2691
|
const value = Number(event["t_can"] ?? 0);
|
|
2690
2692
|
if (!Number.isFinite(value)) return -1;
|
|
2691
|
-
return (Math.min(Math.max(value, 0),
|
|
2693
|
+
return (Math.min(Math.max(value, 0), 10) - 10) / 10;
|
|
2692
2694
|
}
|
|
2693
2695
|
/**
|
|
2694
2696
|
* Host-side authoritative final score: integral of the per-test score over
|
|
@@ -2822,11 +2824,14 @@ var TaskService = class TaskService {
|
|
|
2822
2824
|
const rawStage = String(value.stage ?? "");
|
|
2823
2825
|
const stage = rawStage !== "" ? rawStage : this.inferStage(value, error);
|
|
2824
2826
|
const tCan = Number(value.t_can ?? 0);
|
|
2827
|
+
const successRate = Number(value.success_rate ?? 0);
|
|
2825
2828
|
const finalScore = Number(value.final_score ?? 0);
|
|
2826
2829
|
return {
|
|
2827
2830
|
stage,
|
|
2828
2831
|
t_can: Number.isFinite(tCan) ? tCan : 0,
|
|
2829
|
-
|
|
2832
|
+
success_rate: Number.isFinite(successRate) ? successRate : 0,
|
|
2833
|
+
episodes: Number(value.episodes ?? 0),
|
|
2834
|
+
completed: stage === "completed" && Number.isFinite(successRate) && successRate >= TaskService.FINAL_SUCCESS_TARGET,
|
|
2830
2835
|
error,
|
|
2831
2836
|
final_score: Number.isFinite(finalScore) ? finalScore : 0
|
|
2832
2837
|
};
|
|
@@ -2854,6 +2859,8 @@ var TaskService = class TaskService {
|
|
|
2854
2859
|
submission_id: "",
|
|
2855
2860
|
stage: "no_submission",
|
|
2856
2861
|
t_can: 0,
|
|
2862
|
+
success_rate: 0,
|
|
2863
|
+
episodes: 0,
|
|
2857
2864
|
completed: false,
|
|
2858
2865
|
error: ""
|
|
2859
2866
|
});
|
|
@@ -2869,13 +2876,17 @@ var TaskService = class TaskService {
|
|
|
2869
2876
|
const value = await this.runHostEval(run.workspaceId, startedAt);
|
|
2870
2877
|
const stage = String(value.stage ?? "simulation");
|
|
2871
2878
|
const tCan = Number(value.t_can ?? 0);
|
|
2872
|
-
const
|
|
2879
|
+
const successRate = Number(value.success_rate ?? 0);
|
|
2880
|
+
const episodes = Number(value.episodes ?? 0);
|
|
2881
|
+
const completed = stage === "completed" && Number.isFinite(successRate) && successRate >= TaskService.FINAL_SUCCESS_TARGET;
|
|
2873
2882
|
const event = {
|
|
2874
2883
|
event: "test",
|
|
2875
2884
|
time: requestedAt,
|
|
2876
2885
|
submission_id: latest.id,
|
|
2877
2886
|
stage,
|
|
2878
2887
|
t_can: tCan,
|
|
2888
|
+
success_rate: Number.isFinite(successRate) ? successRate : 0,
|
|
2889
|
+
episodes: Number.isFinite(episodes) ? episodes : 0,
|
|
2879
2890
|
completed,
|
|
2880
2891
|
error: String(value.error ?? "")
|
|
2881
2892
|
};
|
|
@@ -2886,12 +2897,16 @@ var TaskService = class TaskService {
|
|
|
2886
2897
|
at: requestedAt,
|
|
2887
2898
|
stage,
|
|
2888
2899
|
t_can: tCan,
|
|
2900
|
+
success_rate: Number.isFinite(successRate) ? successRate : 0,
|
|
2901
|
+
episodes: Number.isFinite(episodes) ? episodes : 0,
|
|
2889
2902
|
final_score: finalScore
|
|
2890
2903
|
}) + "\n", "utf8");
|
|
2891
2904
|
return {
|
|
2892
2905
|
ok: true,
|
|
2893
2906
|
stage,
|
|
2894
2907
|
t_can: tCan,
|
|
2908
|
+
success_rate: Number.isFinite(successRate) ? successRate : 0,
|
|
2909
|
+
episodes: Number.isFinite(episodes) ? episodes : 0,
|
|
2895
2910
|
completed,
|
|
2896
2911
|
error: String(value.error ?? "")
|
|
2897
2912
|
};
|
|
@@ -3576,6 +3591,8 @@ const OUTPUT_SCHEMA = {
|
|
|
3576
3591
|
files: { type: "number" },
|
|
3577
3592
|
stage: { type: "string" },
|
|
3578
3593
|
t_can: { type: "number" },
|
|
3594
|
+
success_rate: { type: "number" },
|
|
3595
|
+
episodes: { type: "number" },
|
|
3579
3596
|
completed: { type: "boolean" },
|
|
3580
3597
|
error: { type: "string" }
|
|
3581
3598
|
},
|
|
@@ -3613,7 +3630,7 @@ function registerAgentSubmissionTools(agent, guard, tasks) {
|
|
|
3613
3630
|
};
|
|
3614
3631
|
const test = {
|
|
3615
3632
|
name: "eval_test",
|
|
3616
|
-
description: "Run a host-side evaluation of the latest submitted snapshot and return stage, t_can, completed and error. Only successful tests update the score.",
|
|
3633
|
+
description: "Run a host-side evaluation of the latest submitted snapshot and return stage, t_can, success_rate, episodes, completed and error. Only successful tests update the score.",
|
|
3617
3634
|
parameters: {
|
|
3618
3635
|
type: "object",
|
|
3619
3636
|
properties: {}
|
|
@@ -7,14 +7,15 @@ plugin UI.
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
9
9
|
- `run_eval.py` — builds the latest submission, runs the operator oracle, then
|
|
10
|
-
drives `build/control` against
|
|
10
|
+
drives `build/control` against 100 random hidden episodes and emits a
|
|
11
11
|
staged result (`no_submission` / `compile` / `infra` / `controller` /
|
|
12
12
|
`simulation` / `scoring` / `completed`).
|
|
13
13
|
- `ops_eval.py` — operator oracle: PyTorch references computed in-process,
|
|
14
14
|
weighted mean relative error compared with per-operation thresholds. Same
|
|
15
15
|
operations, cases, seeds and thresholds as Neural-FP32 / Neural-CIFAR.
|
|
16
|
-
- `sim_eval` — trusted simulator binary
|
|
17
|
-
|
|
16
|
+
- `sim_eval` — trusted evaluator-only simulator binary. Every reset samples a
|
|
17
|
+
random hidden plant, initial state and observation-noise level; no --eval switch. The workspace `tools/sim` is a separate agent/training
|
|
18
|
+
binary that only does random near-down resets and is not shipped here.
|
|
18
19
|
|
|
19
20
|
## Requirements
|
|
20
21
|
|
|
@@ -44,12 +44,14 @@ DT = 0.01
|
|
|
44
44
|
MAX_STEPS = 3000
|
|
45
45
|
T_TGT = 10.0
|
|
46
46
|
UP_TOL = 0.05
|
|
47
|
+
EPISODES = 100
|
|
48
|
+
SUCCESS_TARGET = 0.8
|
|
47
49
|
# The score changes at the test request instant. This process is started for
|
|
48
50
|
# one request, so module import is the closest available request timestamp.
|
|
49
51
|
TEST_TIME = time.time()
|
|
50
52
|
# Hard budgets so a pathological submission cannot stall the evaluation job.
|
|
51
53
|
OPS_DEADLINE_SECONDS = 240.0
|
|
52
|
-
CONTROL_DEADLINE_SECONDS =
|
|
54
|
+
CONTROL_DEADLINE_SECONDS = 900.0
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
class LineReader:
|
|
@@ -124,6 +126,13 @@ def score_for(event: dict) -> float:
|
|
|
124
126
|
stage = event.get('stage', '')
|
|
125
127
|
if stage not in ('scoring', 'completed') and event.get('valid') is not True:
|
|
126
128
|
return -1.0
|
|
129
|
+
try:
|
|
130
|
+
success_rate = float(event.get('success_rate'))
|
|
131
|
+
except Exception:
|
|
132
|
+
success_rate = float('nan')
|
|
133
|
+
if math.isfinite(success_rate):
|
|
134
|
+
return (min(max(success_rate, 0.0), SUCCESS_TARGET) - SUCCESS_TARGET) / SUCCESS_TARGET
|
|
135
|
+
# Backward-compatible fallback for old single-episode events.
|
|
127
136
|
try:
|
|
128
137
|
t_can = float(event.get('t_can', 0.0))
|
|
129
138
|
except Exception:
|
|
@@ -185,19 +194,23 @@ def emit(result: dict) -> int:
|
|
|
185
194
|
|
|
186
195
|
|
|
187
196
|
def final_score_with_current(workspace: Path, events: list[dict], stage: str,
|
|
188
|
-
t_can: float, success: bool
|
|
197
|
+
t_can: float, success: bool, success_rate: float,
|
|
198
|
+
episodes: int) -> tuple[float, float | None]:
|
|
189
199
|
"""Score including the test this process is performing at TEST_TIME."""
|
|
190
200
|
current = {
|
|
191
201
|
'event': 'test', 'time': TEST_TIME, 'stage': stage,
|
|
192
|
-
't_can': float(t_can), '
|
|
202
|
+
't_can': float(t_can), 'success_rate': float(success_rate),
|
|
203
|
+
'episodes': int(episodes), 'completed': bool(success),
|
|
193
204
|
}
|
|
194
205
|
return final_score(workspace, [*events, current])
|
|
195
206
|
|
|
196
207
|
|
|
197
208
|
def build_result(stage: str, workspace: Path, events: list[dict], error: str = '',
|
|
198
209
|
ops: dict | None = None, source: str = '', t_can: float = 0.0,
|
|
199
|
-
success: bool = False
|
|
200
|
-
|
|
210
|
+
success: bool = False, success_rate: float = 0.0,
|
|
211
|
+
episodes: int = 0) -> dict:
|
|
212
|
+
integral, done_at = final_score_with_current(workspace, events, stage, t_can, success,
|
|
213
|
+
success_rate, episodes)
|
|
201
214
|
result = {
|
|
202
215
|
'task': 'neural-pendulum',
|
|
203
216
|
'ok': True,
|
|
@@ -205,6 +218,8 @@ def build_result(stage: str, workspace: Path, events: list[dict], error: str = '
|
|
|
205
218
|
'infra_ok': False,
|
|
206
219
|
't_can': float(t_can),
|
|
207
220
|
'success': bool(success),
|
|
221
|
+
'success_rate': float(success_rate),
|
|
222
|
+
'episodes': int(episodes),
|
|
208
223
|
'error': error,
|
|
209
224
|
'final_score': integral,
|
|
210
225
|
'done_at': done_at,
|
|
@@ -266,7 +281,10 @@ def main() -> int:
|
|
|
266
281
|
return emit(fail(ops_eval.infrastructure_error(ops), 'infra', workspace, events, ops=ops))
|
|
267
282
|
|
|
268
283
|
env = child_env()
|
|
269
|
-
|
|
284
|
+
# sim_eval is the evaluator-only binary. Every reset samples a fresh
|
|
285
|
+
# hidden plant, initial state and observation-noise level; the success
|
|
286
|
+
# check uses the true state, while the controller only receives obs.
|
|
287
|
+
sim = subprocess.Popen([str(SIM)], stdin=subprocess.PIPE,
|
|
270
288
|
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env)
|
|
271
289
|
control = subprocess.Popen([str(control_path)], cwd=str(source),
|
|
272
290
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
@@ -274,90 +292,135 @@ def main() -> int:
|
|
|
274
292
|
sim_reader = LineReader(sim)
|
|
275
293
|
control_reader = LineReader(control)
|
|
276
294
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
reply = json.loads(sim_reader.readline(10.0))
|
|
280
|
-
obs = reply['obs']
|
|
281
|
-
except Exception as exc:
|
|
282
|
-
return emit(fail('simulator reset failed: ' + str(exc), 'simulation', workspace, events, ops=ops))
|
|
283
|
-
try:
|
|
284
|
-
write_line(control, {'type': 'reset'})
|
|
285
|
-
ready = json.loads(control_reader.readline(10.0))
|
|
286
|
-
except Exception as exc:
|
|
287
|
-
return emit(fail('controller reset failed: ' + str(exc), 'controller', workspace, events, ops=ops))
|
|
288
|
-
if not isinstance(ready, dict) or ready.get('type') != 'ready':
|
|
289
|
-
return emit(fail('controller did not reply ready', 'controller', workspace, events, ops=ops))
|
|
290
|
-
|
|
291
|
-
t_can = 0.0
|
|
292
|
-
up_start = None
|
|
295
|
+
successes = 0
|
|
296
|
+
t_can_total = 0.0
|
|
293
297
|
completed = False
|
|
294
298
|
error = ''
|
|
295
299
|
stage = 'scoring'
|
|
300
|
+
attempted = 0
|
|
296
301
|
control_deadline = time.monotonic() + CONTROL_DEADLINE_SECONDS
|
|
297
|
-
for
|
|
302
|
+
for ep in range(EPISODES):
|
|
298
303
|
if time.monotonic() > control_deadline:
|
|
299
304
|
error = 'control evaluation deadline exceeded'
|
|
300
305
|
stage = 'simulation'
|
|
301
306
|
break
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
stage = 'simulation'
|
|
305
|
-
break
|
|
307
|
+
# A fresh hidden plant, random initial state and fresh observation
|
|
308
|
+
# noise level for every evaluation episode.
|
|
306
309
|
try:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
break
|
|
312
|
-
if not all(math.isfinite(x) for x in angles):
|
|
313
|
-
error = 'simulator returned non-finite observation'
|
|
310
|
+
write_line(sim, {'cmd': 'reset'})
|
|
311
|
+
reply = json.loads(sim_reader.readline(10.0))
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
error = 'simulator reset failed: ' + str(exc)
|
|
314
314
|
stage = 'simulation'
|
|
315
315
|
break
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
up_start = step * DT
|
|
319
|
-
t_can = max(t_can, step * DT - up_start)
|
|
320
|
-
else:
|
|
321
|
-
up_start = None
|
|
322
|
-
if t_can >= T_TGT:
|
|
323
|
-
completed = True
|
|
324
|
-
stage = 'completed'
|
|
325
|
-
break
|
|
326
|
-
|
|
327
|
-
write_line(control, {'type': 'step', 'step': step, 'state': angles})
|
|
316
|
+
obs = reply.get('obs') if isinstance(reply, dict) else None
|
|
317
|
+
true = reply.get('state', obs) if isinstance(reply, dict) else None
|
|
328
318
|
try:
|
|
329
|
-
|
|
319
|
+
write_line(control, {'type': 'reset'})
|
|
320
|
+
ready = json.loads(control_reader.readline(10.0))
|
|
330
321
|
except Exception as exc:
|
|
331
|
-
error = 'controller
|
|
322
|
+
error = 'controller reset failed: ' + str(exc)
|
|
332
323
|
stage = 'controller'
|
|
333
324
|
break
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
or not all(isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
337
|
-
and math.isfinite(v) for v in torque)
|
|
338
|
-
or any(abs(float(v)) > 1.0 for v in torque)):
|
|
339
|
-
error = 'controller returned invalid torque'
|
|
325
|
+
if not isinstance(ready, dict) or ready.get('type') != 'ready':
|
|
326
|
+
error = 'controller did not reply ready'
|
|
340
327
|
stage = 'controller'
|
|
341
328
|
break
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
329
|
+
attempted += 1
|
|
330
|
+
up_start = None
|
|
331
|
+
t_can_ep = 0.0
|
|
332
|
+
failed = False
|
|
333
|
+
for step in range(MAX_STEPS):
|
|
334
|
+
if time.monotonic() > control_deadline:
|
|
335
|
+
error = 'control evaluation deadline exceeded'
|
|
336
|
+
stage = 'simulation'
|
|
337
|
+
failed = True
|
|
338
|
+
break
|
|
339
|
+
if not isinstance(obs, list) or len(obs) != 3:
|
|
340
|
+
error = 'simulator returned bad observation'
|
|
341
|
+
stage = 'simulation'
|
|
342
|
+
failed = True
|
|
343
|
+
break
|
|
344
|
+
try:
|
|
345
|
+
angles = [float(x) for x in obs]
|
|
346
|
+
true_angles = [float(x) for x in true] if isinstance(true, list) and len(true) == 3 else angles
|
|
347
|
+
except Exception:
|
|
348
|
+
error = 'simulator returned non-numeric observation'
|
|
349
|
+
stage = 'simulation'
|
|
350
|
+
failed = True
|
|
351
|
+
break
|
|
352
|
+
if (not all(math.isfinite(x) for x in angles)
|
|
353
|
+
or not all(math.isfinite(x) for x in true_angles)):
|
|
354
|
+
error = 'simulator returned non-finite observation'
|
|
355
|
+
stage = 'simulation'
|
|
356
|
+
failed = True
|
|
357
|
+
break
|
|
358
|
+
# Success is judged on the true state; the controller only sees
|
|
359
|
+
# the noisy angles below.
|
|
360
|
+
if max(abs(x) for x in true_angles) <= UP_TOL:
|
|
361
|
+
if up_start is None:
|
|
362
|
+
up_start = step * DT
|
|
363
|
+
t_can_ep = max(t_can_ep, step * DT - up_start)
|
|
364
|
+
else:
|
|
365
|
+
up_start = None
|
|
366
|
+
if t_can_ep >= T_TGT:
|
|
367
|
+
break
|
|
368
|
+
write_line(control, {'type': 'step', 'step': step, 'state': angles})
|
|
369
|
+
try:
|
|
370
|
+
action = json.loads(control_reader.readline(0.1))
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
error = 'controller did not reply in time: ' + str(exc)
|
|
373
|
+
stage = 'controller'
|
|
374
|
+
failed = True
|
|
375
|
+
break
|
|
376
|
+
torque = action.get('torque') if isinstance(action, dict) else None
|
|
377
|
+
if (not isinstance(torque, list) or len(torque) != 2
|
|
378
|
+
or not all(isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
379
|
+
and math.isfinite(v) for v in torque)
|
|
380
|
+
or any(abs(float(v)) > 1.0 for v in torque)):
|
|
381
|
+
error = 'controller returned invalid torque'
|
|
382
|
+
stage = 'controller'
|
|
383
|
+
failed = True
|
|
384
|
+
break
|
|
385
|
+
write_line(sim, {'cmd': 'step', 'torque': [float(torque[0]), float(torque[1])]})
|
|
386
|
+
try:
|
|
387
|
+
reply = json.loads(sim_reader.readline(10.0))
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
error = 'simulator did not reply: ' + str(exc)
|
|
390
|
+
stage = 'simulation'
|
|
391
|
+
failed = True
|
|
392
|
+
break
|
|
393
|
+
if not isinstance(reply, dict):
|
|
394
|
+
error = 'simulator returned bad reply'
|
|
395
|
+
stage = 'simulation'
|
|
396
|
+
failed = True
|
|
397
|
+
break
|
|
398
|
+
if 'error' in reply:
|
|
399
|
+
error = 'simulator error: ' + str(reply['error'])
|
|
400
|
+
stage = 'simulation'
|
|
401
|
+
failed = True
|
|
402
|
+
break
|
|
403
|
+
obs = reply.get('obs')
|
|
404
|
+
true = reply.get('state', obs)
|
|
405
|
+
if failed:
|
|
352
406
|
break
|
|
353
|
-
if
|
|
354
|
-
|
|
407
|
+
if t_can_ep >= T_TGT:
|
|
408
|
+
successes += 1
|
|
409
|
+
t_can_total += t_can_ep
|
|
410
|
+
if attempted == 0:
|
|
411
|
+
if stage == 'scoring':
|
|
355
412
|
stage = 'simulation'
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
413
|
+
error = error or 'no evaluation episode could be executed'
|
|
414
|
+
success_rate, t_can = 0.0, 0.0
|
|
415
|
+
else:
|
|
416
|
+
success_rate = successes / attempted
|
|
417
|
+
t_can = t_can_total / attempted
|
|
418
|
+
completed = (stage == 'scoring' and attempted == EPISODES
|
|
419
|
+
and success_rate >= SUCCESS_TARGET)
|
|
420
|
+
stage = 'completed' if completed else stage
|
|
359
421
|
result = build_result(stage, workspace, events, error=error, ops=ops, source=source_name,
|
|
360
|
-
t_can=t_can, success=completed
|
|
422
|
+
t_can=t_can, success=completed, success_rate=success_rate,
|
|
423
|
+
episodes=attempted)
|
|
361
424
|
result['infra_ok'] = True
|
|
362
425
|
return emit(result)
|
|
363
426
|
except Exception as exc:
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-eval-guard",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Evaluation guard for DSH: workspace-only filesystem, no network/subagents, wall-clock limits, and task installation for meow-ai-arena and benchmark tasks.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,60 +0,0 @@
|
|
|
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.
|