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.
Files changed (31) hide show
  1. package/ISSUES.md +4 -2
  2. package/README.md +3 -3
  3. package/assets/neural-pendulum/SPEC.md +40 -42
  4. package/assets/neural-pendulum/TASK.md +34 -47
  5. package/assets/neural-pendulum/sim.cpp +97 -45
  6. package/assets/neural-pendulum/sim_agent +0 -0
  7. package/assets/neural-pendulum/sim_eval +0 -0
  8. package/assets/neural-pendulum/workspace.zip +0 -0
  9. package/lib/index.js +27 -10
  10. package/neural-pendulum-eval/README.md +4 -3
  11. package/neural-pendulum-eval/run_eval.py +133 -70
  12. package/neural-pendulum-eval/sim_eval +0 -0
  13. package/package.json +1 -1
  14. package/assets/neural-pendulum/rl-feasibility/README.md +0 -60
  15. package/assets/neural-pendulum/rl-feasibility/RESULTS.md +0 -370
  16. package/assets/neural-pendulum/rl-feasibility/evaluate.py +0 -107
  17. package/assets/neural-pendulum/rl-feasibility/finalize_run.py +0 -168
  18. package/assets/neural-pendulum/rl-feasibility/models.py +0 -124
  19. package/assets/neural-pendulum/rl-feasibility/obs.py +0 -205
  20. package/assets/neural-pendulum/rl-feasibility/plot_selected.py +0 -55
  21. package/assets/neural-pendulum/rl-feasibility/reference_controller.py +0 -112
  22. package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
  23. package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +0 -417
  24. package/assets/neural-pendulum/rl-feasibility/results/summary.json +0 -1599
  25. package/assets/neural-pendulum/rl-feasibility/results/table.md +0 -39
  26. package/assets/neural-pendulum/rl-feasibility/sim.py +0 -324
  27. package/assets/neural-pendulum/rl-feasibility/summarize.py +0 -175
  28. package/assets/neural-pendulum/rl-feasibility/task.py +0 -342
  29. package/assets/neural-pendulum/rl-feasibility/test_contract.py +0 -188
  30. package/assets/neural-pendulum/rl-feasibility/train_ppo.py +0 -403
  31. package/assets/neural-pendulum/rl-feasibility/train_sac.py +0 -406
@@ -1,406 +0,0 @@
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()