dsh-eval-guard 0.7.3 → 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 (58) hide show
  1. package/ISSUES.md +5 -1
  2. package/README.md +4 -2
  3. package/assets/neural-pendulum/SPEC.md +4 -3
  4. package/assets/neural-pendulum/TASK.md +4 -3
  5. package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
  6. package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
  7. package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
  8. package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
  9. package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
  10. package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
  11. package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
  12. package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
  13. package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
  14. package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
  15. package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
  16. package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
  17. package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
  18. package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
  19. package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
  20. package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
  21. package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
  22. package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
  23. package/assets/neural-pendulum/sim.cpp +13 -2
  24. package/assets/neural-pendulum/sim_eval +0 -0
  25. package/assets/neural-pendulum/workspace.zip +0 -0
  26. package/lib/index.js +5 -1
  27. package/native/dsh-eval-supervisor +0 -0
  28. package/native/supervisor.c +42 -10
  29. package/neural-pendulum-eval/sim_eval +0 -0
  30. package/package.json +2 -2
  31. package/eval/__pycache__/bomberman.cpython-310.pyc +0 -0
  32. package/eval/__pycache__/commit0.cpython-310.pyc +0 -0
  33. package/eval/__pycache__/common.cpython-310.pyc +0 -0
  34. package/eval/__pycache__/ctf.cpython-310.pyc +0 -0
  35. package/eval/__pycache__/featurebench.cpython-310.pyc +0 -0
  36. package/eval/__pycache__/land.cpython-310.pyc +0 -0
  37. package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
  38. package/eval/__pycache__/neural-cpp.cpython-310.pyc +0 -0
  39. package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
  40. package/eval/__pycache__/neural-pendulum.cpython-310.pyc +0 -0
  41. package/eval/__pycache__/swe-bench-pro.cpython-310.pyc +0 -0
  42. package/eval/__pycache__/tank.cpython-310.pyc +0 -0
  43. package/eval/__pycache__/warehouse.cpython-310.pyc +0 -0
  44. package/eval/legacy/__pycache__/bomberman.cpython-310.pyc +0 -0
  45. package/eval/legacy/__pycache__/commit0.cpython-310.pyc +0 -0
  46. package/eval/legacy/__pycache__/ctf.cpython-310.pyc +0 -0
  47. package/eval/legacy/__pycache__/featurebench.cpython-310.pyc +0 -0
  48. package/eval/legacy/__pycache__/land.cpython-310.pyc +0 -0
  49. package/eval/legacy/__pycache__/swe_bench_pro.cpython-310.pyc +0 -0
  50. package/eval/legacy/__pycache__/tank.cpython-310.pyc +0 -0
  51. package/eval/legacy/__pycache__/warehouse.cpython-310.pyc +0 -0
  52. package/neural-cifar-eval/__pycache__/cifar_cache.cpython-310.pyc +0 -0
  53. package/neural-cifar-eval/__pycache__/common.cpython-310.pyc +0 -0
  54. package/neural-cifar-eval/__pycache__/fetch_cifar.cpython-310.pyc +0 -0
  55. package/neural-cifar-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
  56. package/neural-cifar-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
  57. package/neural-pendulum-eval/__pycache__/ops_eval.cpython-310.pyc +0 -0
  58. package/neural-pendulum-eval/__pycache__/run_eval.cpython-310.pyc +0 -0
@@ -0,0 +1,406 @@
1
+ """SAC + 2x256 tanh MLP + sin/cos H=4 observation, angle-only simulator.
2
+
3
+ Experiments:
4
+ --init rand : theta_i ~ U(-.05,.05), omega_i ~ U(-.02,.02), no bias
5
+ --init fixed : training reset is exactly theta=[0.013,-0.008,0.021], omega=0
6
+
7
+ 32 vectorised environments by default, replay buffer, auto entropy tuning,
8
+ shared-trunk twin Q, UTD = 0.25 (8 gradient steps per 32-env vector step).
9
+ The simulator exposes angles only; observation, reward, termination, success
10
+ and t_can all live in trainer-side code (obs.py / task.py).
11
+
12
+ TensorBoard scalars (tb/<exp>/):
13
+ train/episode_return, train/episode_length, train/t_can, train/t_can_max,
14
+ train/success_rate, train/ever_up_frac, train/first_up_time, train/up_frac,
15
+ train/action_abs_mean, train/replay_size, train/steps_per_s, train/alpha,
16
+ train/entropy, train/q_mean, train/q_target_mean,
17
+ loss/actor, loss/critic, loss/alpha, loss/policy, loss/value, loss/entropy,
18
+ eval/t_can, eval/success, eval/ep_return, eval/ever_up, eval/final_ang_err,
19
+ eval/first_up_t, eval/up_frac, eval_final/*, time/elapsed_min
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import copy
25
+ import json
26
+ import os
27
+ import time
28
+
29
+ import numpy as np
30
+ import torch
31
+ import torch.nn as nn
32
+ import torch.nn.functional as F
33
+ from torch.utils.tensorboard import SummaryWriter
34
+
35
+ torch.set_num_threads(int(os.environ.get("TORCH_THREADS", "4")))
36
+
37
+ from evaluate import evaluate, evaluate_starts, make_sac_policy
38
+ from models import SquashedGaussianActor, TwinQ
39
+ from obs import OBS_MODES, obs_spec
40
+ from sim import EPISODE_STEPS, Params, TorchVecSim
41
+ from task import (TORCH_REWARD_NOTE, TorchTask, get_reward_preset, make_init,
42
+ sample_train_init)
43
+
44
+
45
+ class Replay:
46
+ def __init__(self, cap, dim, act_dim=2):
47
+ self.cap = int(cap)
48
+ self.obs = np.zeros((cap, dim), np.float32)
49
+ self.act = np.zeros((cap, act_dim), np.float32)
50
+ self.rew = np.zeros((cap, 1), np.float32)
51
+ self.nobs = np.zeros((cap, dim), np.float32)
52
+ self.done = np.zeros((cap, 1), np.float32)
53
+ self.ptr = 0
54
+ self.size = 0
55
+
56
+ def push(self, obs, act, rew, nobs, done):
57
+ n = obs.shape[0]
58
+ idx = (self.ptr + np.arange(n)) % self.cap
59
+ self.obs[idx] = obs
60
+ self.act[idx] = act
61
+ self.rew[idx] = np.asarray(rew).reshape(n, 1)
62
+ self.nobs[idx] = nobs
63
+ self.done[idx] = np.asarray(done).reshape(n, 1)
64
+ self.ptr = (self.ptr + n) % self.cap
65
+ self.size = min(self.cap, self.size + n)
66
+
67
+ def sample(self, batch):
68
+ idx = np.random.randint(0, self.size, size=batch)
69
+ return (torch.from_numpy(self.obs[idx]),
70
+ torch.from_numpy(self.act[idx]),
71
+ torch.from_numpy(self.rew[idx]),
72
+ torch.from_numpy(self.nobs[idx]),
73
+ torch.from_numpy(self.done[idx]))
74
+
75
+
76
+ def soft_update(tgt, src, tau):
77
+ with torch.no_grad():
78
+ for a, b in zip(tgt.parameters(), src.parameters()):
79
+ a.mul_(1 - tau).add_(tau * b)
80
+
81
+
82
+ def sac_update(actor, q, qt, act_opt, q_opt, log_alpha, alpha_opt, replay, args):
83
+ o, ac, r, no, d = replay.sample(args.batch)
84
+ with torch.no_grad():
85
+ na, nlogp = actor.sample(no)
86
+ q_next = qt(no, na).min(dim=1).values
87
+ target = r.squeeze(-1) + args.gamma * (1.0 - d.squeeze(-1)) * (
88
+ q_next - log_alpha.exp() * nlogp)
89
+ q_pred = q(o, ac)
90
+ critic_loss = 0.5 * F.mse_loss(q_pred, target.unsqueeze(1).expand(-1, 2))
91
+ q_opt.zero_grad()
92
+ critic_loss.backward()
93
+ nn.utils.clip_grad_norm_(q.parameters(), 10.0)
94
+ q_opt.step()
95
+
96
+ for p_ in q.parameters():
97
+ p_.requires_grad_(False)
98
+ pa, logp = actor.sample(o)
99
+ q_pi = q(o, pa).min(dim=1).values
100
+ actor_loss = (log_alpha.exp().detach() * logp - q_pi).mean()
101
+ act_opt.zero_grad()
102
+ actor_loss.backward()
103
+ nn.utils.clip_grad_norm_(actor.parameters(), 10.0)
104
+ act_opt.step()
105
+ for p_ in q.parameters():
106
+ p_.requires_grad_(True)
107
+
108
+ alpha_loss = -(log_alpha.exp() * (logp.detach() + args.target_entropy)).mean()
109
+ alpha_opt.zero_grad()
110
+ alpha_loss.backward()
111
+ alpha_opt.step()
112
+
113
+ soft_update(qt, q, args.tau)
114
+ return dict(actor=float(actor_loss), critic=float(critic_loss),
115
+ alpha=float(alpha_loss),
116
+ q=float(q_pred.mean()), qt=float(target.mean()),
117
+ entropy=float(-logp.detach().mean()))
118
+
119
+
120
+ def main():
121
+ ap = argparse.ArgumentParser()
122
+ ap.add_argument("--exp", type=str, required=True)
123
+ ap.add_argument("--seed", type=int, default=0)
124
+ ap.add_argument("--init", type=str, default="rand",
125
+ choices=["rand", "fixed", "mix"],
126
+ help="rand=down noise, fixed=eval state, "
127
+ "mix=curriculum with up_frac near upright")
128
+ ap.add_argument("--up-frac", type=float, default=0.5,
129
+ help="fraction of resets near upright when --init mix")
130
+ ap.add_argument("--obs", type=str, default="full",
131
+ choices=sorted(OBS_MODES),
132
+ help="observation mode (see obs.py)")
133
+ ap.add_argument("--reward", type=str, default="base",
134
+ choices=["base", "sparse", "energy"],
135
+ help="reward preset (see task.py REWARD_PRESETS)")
136
+ ap.add_argument("--envs", type=int, default=32)
137
+ ap.add_argument("--minutes", type=float, default=30.0)
138
+ ap.add_argument("--steps", type=int, default=10 ** 12)
139
+ ap.add_argument("--hidden", type=int, default=256)
140
+ ap.add_argument("--depth", type=int, default=2)
141
+ ap.add_argument("--lr", type=float, default=3e-4)
142
+ ap.add_argument("--gamma", type=float, default=0.99)
143
+ ap.add_argument("--tau", type=float, default=0.005)
144
+ ap.add_argument("--batch", type=int, default=256)
145
+ ap.add_argument("--warmup", type=int, default=5000)
146
+ ap.add_argument("--buffer", type=int, default=300000)
147
+ ap.add_argument("--utd", type=float, default=0.25,
148
+ help="gradient steps per environment step")
149
+ ap.add_argument("--target-entropy", type=float, default=-2.0)
150
+ ap.add_argument("--alpha-init", type=float, default=0.2)
151
+ ap.add_argument("--episode-steps", type=int, default=EPISODE_STEPS)
152
+ ap.add_argument("--eval-every", type=int, default=20000)
153
+ ap.add_argument("--eval-start", type=int, default=20000)
154
+ ap.add_argument("--tb-root", type=str, default="tb")
155
+ ap.add_argument("--ckpt-dir", type=str, default="checkpoints")
156
+ ap.add_argument("--out", type=str, default="results")
157
+ ap.add_argument("--tau-max", type=float, default=18.0)
158
+ ap.add_argument("--damping", type=float, default=0.01)
159
+ ap.add_argument("--log-every", type=int, default=10,
160
+ help="log every N vector steps")
161
+ ap.add_argument("--robust-starts", type=int, default=8,
162
+ help="deterministic extra rollouts from the training "
163
+ "reset distribution at the end (0 disables)")
164
+ args = ap.parse_args()
165
+
166
+ torch.manual_seed(args.seed)
167
+ np.random.seed(args.seed)
168
+ for d in (args.tb_root, args.ckpt_dir, args.out):
169
+ os.makedirs(d, exist_ok=True)
170
+ exp = args.exp
171
+ writer = SummaryWriter(os.path.join(args.tb_root, exp))
172
+ p = Params(tau_max=args.tau_max, b=args.damping)
173
+ spec = obs_spec(args.obs)
174
+ obs_dim = spec["dim"]
175
+ reward_w = get_reward_preset(args.reward)
176
+ sim = TorchVecSim(p, n=args.envs, episode_steps=args.episode_steps)
177
+ task = TorchTask(p, n=args.envs, reward_w=reward_w, obs_mode=args.obs)
178
+ gen = torch.Generator().manual_seed(args.seed * 1000 + 11)
179
+ updates_per_vec = max(1, int(round(args.utd * args.envs)))
180
+ eff_utd = updates_per_vec / args.envs
181
+
182
+ init_meta = dict(mode=args.init,
183
+ training=("theta_i ~ U(-0.05,0.05), omega_i ~ U(-0.02,0.02); no bias"
184
+ if args.init == "rand" else
185
+ ("theta=[0.013,-0.008,0.021], omega=0 (same as eval)"
186
+ if args.init == "fixed" else
187
+ f"curriculum: {args.up_frac:.2f} near upright "
188
+ f"(pi + U(-0.05,0.05)), rest near down")),
189
+ eval="theta=[0.013,-0.008,0.021], omega=0 (fixed, no noise)")
190
+ meta = dict(algo="sac", exp=exp, seed=args.seed, envs=args.envs,
191
+ obs=spec, obs_dim=obs_dim, init=init_meta,
192
+ reward=reward_w, reward_preset=args.reward,
193
+ reward_note=TORCH_REWARD_NOTE,
194
+ updates_per_vector=updates_per_vec, effective_utd=eff_utd,
195
+ args=vars(args))
196
+ writer.add_text("config", json.dumps(meta, indent=2), 0)
197
+ print(json.dumps(meta, indent=2), flush=True)
198
+
199
+ actor = SquashedGaussianActor(obs_dim, 2, hidden=args.hidden, depth=args.depth)
200
+ q = TwinQ(obs_dim, 2, hidden=args.hidden, depth=args.depth)
201
+ qt = copy.deepcopy(q)
202
+ act_opt = torch.optim.Adam(actor.parameters(), lr=args.lr)
203
+ q_opt = torch.optim.Adam(q.parameters(), lr=args.lr)
204
+ log_alpha = torch.log(torch.tensor(args.alpha_init)).clone().detach().requires_grad_(True)
205
+ alpha_opt = torch.optim.Adam([log_alpha], lr=args.lr)
206
+
207
+ replay = Replay(args.buffer, obs_dim, 2)
208
+ th, om = make_init(args.envs, args.init, gen, up_frac=args.up_frac)
209
+ theta = sim.reset(th, om)
210
+ obs = task.reset(theta)
211
+
212
+ t_start = time.time()
213
+ total = 0
214
+ n_updates = 0
215
+ vec_steps = 0
216
+ next_eval = args.eval_start
217
+ best_t_can = -1.0
218
+ best_eval = None
219
+ best_path = None
220
+ last_path = None
221
+ eval_curve = []
222
+ last = dict(actor=float("nan"), critic=float("nan"), alpha=float("nan"),
223
+ q=float("nan"), qt=float("nan"), entropy=float("nan"))
224
+ ep = {k: [] for k in ("return", "t_can", "ever_up", "first_up", "up_frac", "length")}
225
+
226
+ def save_ckpt(path, ev):
227
+ torch.save(dict(model=actor.state_dict(), q=q.state_dict(),
228
+ log_alpha=log_alpha.detach(), obs=spec,
229
+ obs_dim=obs_dim, init=init_meta, reward=reward_w,
230
+ reward_preset=args.reward,
231
+ args=vars(args), global_steps=total,
232
+ n_updates=n_updates, eval=ev, algo="sac"),
233
+ os.path.join(args.ckpt_dir, path))
234
+
235
+ def log_eval(ev):
236
+ nonlocal best_t_can, best_eval, best_path, last_path
237
+ eval_curve.append(dict(global_steps=total, n_updates=n_updates, **ev))
238
+ for k, v in ev.items():
239
+ if isinstance(v, (int, float)):
240
+ writer.add_scalar(f"eval/{k}", v, total)
241
+ if (ev["t_can"] > best_t_can
242
+ or (ev["t_can"] == best_t_can and best_eval is not None
243
+ and ev["ep_return"] > best_eval["ep_return"])):
244
+ best_t_can = ev["t_can"]
245
+ best_eval = dict(ev)
246
+ best_path = exp + "_best.pt"
247
+ save_ckpt(best_path, ev)
248
+ last_path = exp + "_last.pt"
249
+ save_ckpt(last_path, ev)
250
+
251
+ while total < args.steps:
252
+ if (time.time() - t_start) / 60.0 > args.minutes:
253
+ print("time budget reached", flush=True)
254
+ break
255
+ if replay.size < args.warmup:
256
+ u = torch.rand(args.envs, 2) * 2.0 - 1.0
257
+ else:
258
+ with torch.no_grad():
259
+ u, _ = actor.sample(obs)
260
+ u = torch.clamp(u, -1.0, 1.0)
261
+ theta = sim.step(u)
262
+ nobs, rew = task.step(theta, u)
263
+ done = sim.steps >= sim.episode_steps
264
+ replay.push(obs.detach().cpu().numpy(), u.detach().cpu().numpy(),
265
+ rew.detach().cpu().numpy(), nobs.detach().cpu().numpy(),
266
+ done.detach().cpu().numpy())
267
+ if bool(done.any()):
268
+ idx = done.nonzero().flatten()
269
+ summ = task.summary(idx)
270
+ ep["return"] += summ["ep_return"].tolist()
271
+ ep["t_can"] += summ["t_can"].tolist()
272
+ ep["ever_up"] += summ["ever_up"].tolist()
273
+ ep["first_up"] += summ["first_up_t"].tolist()
274
+ ep["up_frac"] += summ["up_frac"].tolist()
275
+ ep["length"] += summ["length"].tolist()
276
+ th, om = make_init(idx.numel(), args.init, gen, up_frac=args.up_frac)
277
+ reset_theta = sim.reset(th, om, idx)
278
+ task.reset(reset_theta, idx)
279
+ nobs = task.obs
280
+ obs = nobs
281
+ total += args.envs
282
+ vec_steps += 1
283
+
284
+ if replay.size >= args.warmup:
285
+ for _ in range(updates_per_vec):
286
+ last = sac_update(actor, q, qt, act_opt, q_opt, log_alpha,
287
+ alpha_opt, replay, args)
288
+ n_updates += 1
289
+
290
+ if vec_steps % args.log_every == 0:
291
+ elapsed = time.time() - t_start
292
+ if ep["return"]:
293
+ writer.add_scalar("train/episode_return",
294
+ float(np.mean(ep["return"])), total)
295
+ writer.add_scalar("train/episode_length",
296
+ float(np.mean(ep["length"])), total)
297
+ writer.add_scalar("train/t_can", float(np.mean(ep["t_can"])), total)
298
+ writer.add_scalar("train/t_can_max", float(np.max(ep["t_can"])), total)
299
+ writer.add_scalar("train/success_rate",
300
+ float(np.mean(np.asarray(ep["t_can"]) >= 10.0)), total)
301
+ writer.add_scalar("train/ever_up_frac",
302
+ float(np.mean(ep["ever_up"])), total)
303
+ writer.add_scalar("train/up_frac",
304
+ float(np.mean(ep["up_frac"])), total)
305
+ fu = [x for x in ep["first_up"] if x > 0]
306
+ if fu:
307
+ writer.add_scalar("train/first_up_time", float(np.mean(fu)), total)
308
+ ep = {k: [] for k in ep}
309
+ writer.add_scalar("train/replay_size", replay.size, total)
310
+ writer.add_scalar("train/steps_per_s", total / max(elapsed, 1e-9), total)
311
+ writer.add_scalar("train/alpha", float(log_alpha.detach().exp()), total)
312
+ writer.add_scalar("train/action_abs_mean", float(u.abs().mean()), total)
313
+ writer.add_scalar("train/action_std", float(u.std()), total)
314
+ writer.add_scalar("loss/actor", last["actor"], total)
315
+ writer.add_scalar("loss/critic", last["critic"], total)
316
+ writer.add_scalar("loss/alpha", last["alpha"], total)
317
+ writer.add_scalar("loss/policy", last["actor"], total)
318
+ writer.add_scalar("loss/value", last["critic"], total)
319
+ writer.add_scalar("loss/entropy", last["entropy"], total)
320
+ writer.add_scalar("train/entropy", last["entropy"], total)
321
+ writer.add_scalar("train/q_mean", last["q"], total)
322
+ writer.add_scalar("train/q_target_mean", last["qt"], total)
323
+ writer.add_scalar("train/global_steps", total, total)
324
+ writer.add_scalar("time/elapsed_min", elapsed / 60.0, total)
325
+ writer.flush()
326
+
327
+ if total >= next_eval:
328
+ next_eval += args.eval_every
329
+ ev = evaluate(make_sac_policy(actor), p, args.episode_steps,
330
+ reward_w=reward_w, obs_mode=args.obs)
331
+ log_eval(ev)
332
+ print(f"steps {total/1e3:7.1f}k upd {n_updates:6d} "
333
+ f"EVAL tc {ev['t_can']:6.2f} succ {ev['success']:.0f} "
334
+ f"everup {ev['ever_up']:.0f} angerr {ev['final_ang_err']:.3f} "
335
+ f"ret {ev['ep_return']:8.1f} q {last['q']:8.2f} "
336
+ f"a {float(log_alpha.detach().exp()):.2f} "
337
+ f"({time.time()-t_start:.0f}s)", flush=True)
338
+ writer.flush()
339
+
340
+ ev_final = evaluate(make_sac_policy(actor), p, args.episode_steps,
341
+ reward_w=reward_w, obs_mode=args.obs)
342
+ eval_curve.append(dict(global_steps=total, n_updates=n_updates, **ev_final))
343
+ for k, v in ev_final.items():
344
+ if isinstance(v, (int, float)):
345
+ writer.add_scalar(f"eval_final/{k}", v, total)
346
+ final_path = exp + "_final.pt"
347
+ save_ckpt(final_path, ev_final)
348
+ last_path = exp + "_last.pt"
349
+ save_ckpt(last_path, ev_final)
350
+
351
+ # Extra deterministic rollouts from the training reset distribution. This
352
+ # is NOT the mandated fixed-state eval; it checks generalisation.
353
+ rob = None
354
+ rob_best = None
355
+ if args.robust_starts > 0:
356
+ g = torch.Generator().manual_seed(args.seed * 1000 + 123)
357
+ th_r, om_r = sample_train_init(args.robust_starts, gen=g)
358
+ starts = list(zip(th_r.numpy(), om_r.numpy()))
359
+ rob = evaluate_starts(make_sac_policy(actor), p, starts,
360
+ args.episode_steps, reward_w=reward_w,
361
+ obs_mode=args.obs)
362
+ for k, v in rob.items():
363
+ if isinstance(v, (int, float)):
364
+ writer.add_scalar(f"robust/{k}", v, total)
365
+ if best_path is not None:
366
+ ck = torch.load(os.path.join(args.ckpt_dir, best_path),
367
+ map_location="cpu")
368
+ actor_best = SquashedGaussianActor(obs_dim, 2, hidden=args.hidden,
369
+ depth=args.depth)
370
+ actor_best.load_state_dict(ck["model"])
371
+ rob_best = evaluate_starts(make_sac_policy(actor_best), p, starts,
372
+ args.episode_steps, reward_w=reward_w,
373
+ obs_mode=args.obs)
374
+ for k, v in rob_best.items():
375
+ if isinstance(v, (int, float)):
376
+ writer.add_scalar(f"robust_best/{k}", v, total)
377
+
378
+ wall = time.time() - t_start
379
+ out = dict(algo="sac", exp=exp, seed=args.seed, envs=args.envs,
380
+ obs=spec, obs_dim=obs_dim, init=init_meta,
381
+ reward=reward_w, reward_preset=args.reward,
382
+ reward_note=TORCH_REWARD_NOTE,
383
+ args=vars(args), updates_per_vector=updates_per_vec,
384
+ effective_utd=eff_utd, minutes_budget=args.minutes,
385
+ global_steps=total, gradient_steps=n_updates,
386
+ wall_time_s=wall, steps_per_s=total / max(wall, 1e-9),
387
+ updates_per_s=n_updates / max(wall, 1e-9),
388
+ final_eval=ev_final, best_eval=best_eval, best_t_can=best_t_can,
389
+ eval_curve=eval_curve, robust=rob, robust_best=rob_best,
390
+ final_train=last,
391
+ checkpoints=dict(best=best_path, last=last_path,
392
+ final=final_path))
393
+ with open(os.path.join(args.out, exp + ".json"), "w") as f:
394
+ json.dump(out, f, indent=2)
395
+ writer.flush()
396
+ writer.close()
397
+ print("DONE " + json.dumps(dict(
398
+ exp=exp, init=args.init, seed=args.seed, envs=args.envs,
399
+ global_steps=total, gradient_steps=n_updates, wall_s=round(wall, 1),
400
+ steps_per_s=round(out["steps_per_s"], 1),
401
+ eval_t_can=ev_final["t_can"], best_t_can=best_t_can,
402
+ eval_return=ev_final["ep_return"])), flush=True)
403
+
404
+
405
+ if __name__ == "__main__":
406
+ main()
@@ -65,7 +65,13 @@ static void deriv(const State& s, const std::array<double,N>& tau, State& out) {
65
65
  std::array<double,N> C{}, G{}, Q{}, rhs{};
66
66
  for(int i=0;i<N;i++){
67
67
  double ci=0;
68
- for(int j=0;j<N;j++) if(j!=i) ci += M[i][j] * std::sin(s.theta[i]-s.theta[j]) * s.omega[j]*s.omega[j];
68
+ for(int j=0;j<N;j++) if(j!=i) {
69
+ // Coriolis coefficient: d(M_ij)/d(theta_j) = LEN[min] * GCOEF[max] * sin(...);
70
+ // M_ij itself already contains cos(...), so multiplying M_ij by sin would
71
+ // add an extra cos factor and inject energy at high speed (NaN after ~7 s).
72
+ const int a=std::min(i,j), b=std::max(i,j);
73
+ ci += LEN[a] * GCOEF[b] * std::sin(s.theta[i]-s.theta[j]) * s.omega[j]*s.omega[j];
74
+ }
69
75
  C[i]=ci;
70
76
  G[i]=GRAV*std::sin(s.theta[i])*GCOEF[i];
71
77
  }
@@ -101,7 +107,12 @@ int main(int argc, char** argv) {
101
107
  // parse torque array
102
108
  double u1=0,u3=0; auto p=line.find('['); if(p!=std::string::npos){ char* end=nullptr; u1=std::strtod(line.c_str()+p+1,&end); if(end){ const char* q=end; while(*q && (*q==','||*q==' ')) q++; u3=std::strtod(q,&end); } }
103
109
  u1=std::max(-1.0,std::min(1.0,u1)); u3=std::max(-1.0,std::min(1.0,u3));
104
- std::array<double,N> tau{{u1*TAU_MAX,0.0,u3*TAU_MAX}};
110
+ // Joint 3 is an internal motor between link 2 and link 3. In absolute
111
+ // coordinates its generalized force is (tau1, -tau3, tau3): the reaction
112
+ // on link 2 is part of the same joint torque. (tau1, 0, tau3) would be a
113
+ // reactionless external torque on link 3 and would contradict joint 2
114
+ // being passive.
115
+ std::array<double,N> tau{{u1*TAU_MAX,-u3*TAU_MAX,u3*TAU_MAX}};
105
116
  // one control step with 4 RK4 substeps
106
117
  double h=DT/SUB;
107
118
  for(int sub=0;sub<SUB;sub++){
Binary file
package/lib/index.js CHANGED
@@ -887,6 +887,8 @@ function bwrapArgs(workspace, rules, runtime) {
887
887
  args.push("--setenv", "LC_ALL", "C.UTF-8");
888
888
  args.push("--setenv", "TERM", "dumb");
889
889
  args.push("--setenv", "UV_USE_IO_URING", "0");
890
+ args.push("--setenv", "PIP_DISABLE_PIP_VERSION_CHECK", "1");
891
+ args.push("--setenv", "PIP_NO_INPUT", "1");
890
892
  return args;
891
893
  }
892
894
  /**
@@ -2480,10 +2482,12 @@ const NEURAL_PENDULUM_DEFINITION = {
2480
2482
  "",
2481
2483
  "请先完整阅读 TASK.md;它是权威规则。硬约束:仅 C++17 标准库、仅 CPU、fp32;不使用 Python/OpenCL/CUDA/BLAS/Eigen/第三方库;必须从提交源码构建出 build/nn_task 与 build/control。",
2482
2484
  "",
2483
- "环境细节:joint1 与 joint3 驱动,joint2 被动;控制频率 100 Hz;每回合最多 3000 步(30 s),达到上限后继续 step 返回 {\"error\":\"episode_finished\"},必须 reset。环境只返回三个关节角度(弧度,不除 pi),不返回 reward/loss/cost/done;环境不提供杆长、质量、惯量、角速度或 tip。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
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。训练 reset 带小范围随机初始角度和角速度;评测使用固定初始状态,无随机扰动。",
2484
2486
  "",
2485
2487
  "优化目标:从接近全向下起摆,使三根杆全部进入 0.05 rad 的竖直向上容差,并尽可能长时间保持;评测指标为 t_can。损失函数、奖励、优化器、网络结构和训练方法由你自行设计,不规定任何训练方案。",
2486
2488
  "",
2489
+ "注意:评测侧用 PyTorch 计算算子参考值,但工作区/候选运行时不提供 PyTorch 或 numpy,也没有网络,无法 pip 安装;开发阶段请用 C++ 或 Python 标准库/独立 double 实现做数值对照。",
2490
+ "",
2487
2491
  "评测会从干净副本重新构建提交源码,先运行与 Neural-FP32/Neural-CIFAR 相同的算子数值评测:评测侧用 PyTorch 在相同数据上计算参考值,逐操作比较加权平均相对误差并对照阈值,任一算子不可执行、崩溃、超时、输出缺失或误差超阈值即判为基础设施失败;然后在固定初始状态上驱动 build/control,统计连续全向上保持时间 t_can。不要把任何未提交的二进制、临时构建产物或外部进程当作交付依赖。",
2488
2492
  "使用宿主工具 eval_submit 提交当前源码快照,使用 eval_test 请求宿主评测最新提交。提交快照、事件、测试结果和最终分数全部保存在工作区之外的宿主目录;不要尝试读取或修改这些数据,也不要依赖任何工作区内的测试结果文件。最终评测使用最新提交快照,按宿主事件时间积分计算分数。",
2489
2493
  "",
Binary file
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Installs a seccomp user-notification filter before exec'ing bwrap and enforces
5
5
  * a path policy on file-access and network syscalls. Normal syscalls are not
6
- * logged; only denied operations are appended to the audit file and cause the
7
- * whole process group to be killed.
6
+ * logged. File-policy violations are audited and kill the process group;
7
+ * AF_INET/AF_INET6 connect/bind/sendto are audited and soft-denied with EPERM
8
+ * so local software checks (pip version probe, NSS fallback) can continue.
8
9
  *
9
10
  * Usage:
10
11
  * dsh-eval-supervisor --policy <policy-file> --audit <audit-file> -- <program> [args...]
@@ -87,6 +88,7 @@ static int g_dev_count;
87
88
  static Rule g_rules[MAX_RULES];
88
89
  static int g_rule_count;
89
90
  static int g_audit_fd = -1;
91
+ static int g_soft_deny; /* network-family denial: EPERM, do not kill the group */
90
92
  static volatile sig_atomic_t g_stop;
91
93
  /* The sandbox mounted a private tmpfs at /tmp (workspace under host /tmp). */
92
94
  static int g_tmpfs_tmp;
@@ -555,6 +557,7 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
555
557
 
556
558
  path_out[0] = '\0';
557
559
  g_current_pid = pid;
560
+ g_soft_deny = 0;
558
561
  if (nr == SYS_execve || nr == SYS_execveat) {
559
562
  char executable[MAX_PATH];
560
563
  executable[0] = '\0';
@@ -567,20 +570,43 @@ static int check_notification(pid_t pid, const struct seccomp_notif *req, int *w
567
570
  return 1;
568
571
  }
569
572
  if (nr == SYS_socket) {
570
- int domain = (int)a0;
573
+ /* Creating a socket does not transmit anything. bwrap already put the
574
+ * command in a private network namespace; tools such as pip create an
575
+ * AF_INET socket for local/version checks without connecting. All the
576
+ * operations that can actually reach a peer (connect/bind/listen/accept/
577
+ * sendto/recvfrom) remain denied below. */
571
578
  *write_access = 0;
572
579
  snprintf(path_out, MAX_PATH, "<network:socket>");
573
- /* AF_UNIX/AF_NETLINK stay inside the private network namespace and are
574
- * needed by NSS, Python and the dynamic loader. AF_INET/AF_INET6 and raw
575
- * sockets are still blocked. */
576
- if (domain == AF_UNIX || domain == AF_NETLINK) return 1;
577
- return 0;
580
+ return 1;
578
581
  }
579
582
  if (is_network_syscall(nr)) {
580
583
  *write_access = 0;
581
584
  snprintf(path_out, MAX_PATH, "<network:%s>", syscall_name(nr));
582
- /* Only AF_UNIX/AF_NETLINK sockets can exist now; their traffic is local
583
- * to the private namespace and cannot reach the host network. */
585
+ /* bwrap itself needs AF_UNIX bind/listen/connect while it sets up the
586
+ * namespace; only the target's traffic is checked. */
587
+ if (!g_command_started || (g_current_pid > 0 && is_bwrap(g_current_pid))) return 1;
588
+ if (nr == SYS_listen || nr == SYS_accept || nr == SYS_accept4 || nr == SYS_recvfrom) {
589
+ /* No peer can appear outside the private netns; these operate on an
590
+ * already-authorized local socket. */
591
+ return 1;
592
+ }
593
+ if (nr == SYS_connect || nr == SYS_bind || nr == SYS_sendto) {
594
+ unsigned long addr_ptr = nr == SYS_sendto ? (unsigned long)req->data.args[4] : a1;
595
+ unsigned short family = 0;
596
+ if (addr_ptr != 0 && read_remote(pid, addr_ptr, &family, sizeof(family)) == 0) {
597
+ if (family == AF_UNIX || family == AF_NETLINK) return 1;
598
+ if (family == AF_INET || family == AF_INET6) {
599
+ /* A version check or NSS probe may create the socket and attempt a
600
+ * connection; return EPERM instead of killing the whole group. The
601
+ * private netns is the hard boundary; this is the graceful denial. */
602
+ g_soft_deny = 1;
603
+ snprintf(path_out, MAX_PATH, "<network:%s:inet>", syscall_name(nr));
604
+ return 0;
605
+ }
606
+ }
607
+ g_soft_deny = 1;
608
+ return 0;
609
+ }
584
610
  return 1;
585
611
  }
586
612
  if (nr == SYS_io_uring_setup || nr == SYS_ptrace || nr == SYS_open_by_handle_at) {
@@ -817,6 +843,12 @@ static void reply_loop(int fd) {
817
843
  char syscall_buf[64];
818
844
  snprintf(syscall_buf, sizeof(syscall_buf), "%s", syscall_name((long)req.data.nr));
819
845
  audit_record((pid_t)req.pid, syscall_buf, path, write_access);
846
+ if (g_soft_deny) {
847
+ /* A software check (pip version probe, NSS lookup fallback) gets a
848
+ * normal EPERM and can continue; file-policy violations still kill. */
849
+ respond(fd, &req, 0);
850
+ continue;
851
+ }
820
852
  /* io_uring is a soft denial: libuv probes it despite UV_USE_IO_URING=0
821
853
  * and must receive EPERM and fall back to epoll instead of taking the
822
854
  * whole command group down. No io_uring instance can exist, so the
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-eval-guard",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
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",
@@ -48,7 +48,7 @@
48
48
  "build": "tsdown && node scripts/build-native.mjs",
49
49
  "build:native": "node scripts/build-native.mjs",
50
50
  "prepare": "tsdown",
51
- "test": "node test/simulate.ts && node test/scoring.ts && node test/jobs.ts",
51
+ "test": "node test/simulate.ts && node test/scoring.ts && node test/jobs.ts && node test/pendulum-sim.ts",
52
52
  "typecheck": "tsc --noEmit -p tsconfig.json"
53
53
  },
54
54
  "dsh": {