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,403 +0,0 @@
1
- """PPO + 2x128 tanh MLP + sin/cos H=4 observation, angle-only simulator.
2
-
3
- Experiments:
4
- --init rand : every training reset draws theta_i ~ U(-.05,.05) and
5
- omega_i ~ U(-.02,.02), with no fixed bias.
6
- --init fixed : every training reset is exactly the evaluation state
7
- theta = [0.013,-0.008,0.021], omega = 0.
8
-
9
- The simulator returns only theta and raises EpisodeLengthError after 3000
10
- steps. Everything else -- observation, reward, done, success, t_can, return
11
- -- is built here and in task.py, never in the simulator.
12
-
13
- TensorBoard scalars (tb/<exp>/):
14
- train/episode_return, train/episode_length, train/t_can, train/t_can_max,
15
- train/success_rate, train/ever_up_frac, train/first_up_time,
16
- train/mean_step_reward, train/action_abs_mean, train/std, train/lr,
17
- train/approx_kl, train/clip_frac,
18
- loss/policy, loss/value, loss/entropy,
19
- eval/t_can, eval/success, eval/ep_return, eval/ever_up, eval/final_ang_err,
20
- eval/first_up_t, eval/up_frac, eval_final/*,
21
- train/global_steps, time/elapsed_min
22
- """
23
- from __future__ import annotations
24
-
25
- import argparse
26
- import json
27
- import os
28
- import time
29
-
30
- import numpy as np
31
- import torch
32
- import torch.nn as nn
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_ppo_policy
38
- from models import ActorCritic
39
- from obs import OBS_MODES, obs_spec
40
- from sim import EPISODE_STEPS, Params, TorchVecSim
41
- from task import (EVAL_THETA0, TORCH_REWARD_NOTE, TorchTask,
42
- get_reward_preset, make_init, sample_train_init)
43
-
44
-
45
- def collect(sim, task, ac, T, args, gen, ep_stats):
46
- B = sim.n
47
- obs = task.obs
48
- obs_buf = torch.zeros(T, B, obs.shape[-1])
49
- act_buf = torch.zeros(T, B, 2)
50
- logp_buf = torch.zeros(T, B)
51
- rew_buf = torch.zeros(T, B)
52
- done_buf = torch.zeros(T, B)
53
- val_buf = torch.zeros(T, B)
54
- for t in range(T):
55
- with torch.no_grad():
56
- dist = ac.dist(obs)
57
- a = dist.sample()
58
- logp = dist.log_prob(a).sum(-1)
59
- v = ac.value(obs)
60
- theta = sim.step(a)
61
- nobs, rew = task.step(theta, a)
62
- done = sim.steps >= sim.episode_steps
63
- obs_buf[t] = obs
64
- act_buf[t] = a
65
- logp_buf[t] = logp
66
- rew_buf[t] = rew
67
- done_buf[t] = done.float()
68
- val_buf[t] = v
69
- if bool(done.any()):
70
- idx = done.nonzero().flatten()
71
- summ = task.summary(idx)
72
- ep_stats["return"] += summ["ep_return"].tolist()
73
- ep_stats["t_can"] += summ["t_can"].tolist()
74
- ep_stats["ever_up"] += summ["ever_up"].tolist()
75
- ep_stats["first_up"] += summ["first_up_t"].tolist()
76
- ep_stats["up_frac"] += summ["up_frac"].tolist()
77
- ep_stats["length"] += summ["length"].tolist()
78
- th, om = make_init(idx.numel(), args.init, gen, up_frac=args.up_frac)
79
- reset_theta = sim.reset(th, om, idx)
80
- task.reset(reset_theta, idx)
81
- nobs = task.obs
82
- obs = nobs
83
- with torch.no_grad():
84
- last_v = ac.value(obs)
85
- return dict(obs=obs_buf, act=act_buf, logp=logp_buf, rew=rew_buf,
86
- done=done_buf, val=val_buf, last_v=last_v, last_obs=obs)
87
-
88
-
89
- def gae(rb, vb, db, last_v, gamma, lam):
90
- T, B = rb.shape
91
- adv = torch.zeros(T, B)
92
- last = torch.zeros(B)
93
- for t in reversed(range(T)):
94
- next_v = last_v if t == T - 1 else vb[t + 1]
95
- nonterm = 1.0 - db[t]
96
- delta = rb[t] + gamma * next_v * nonterm - vb[t]
97
- last = delta + gamma * lam * nonterm * last
98
- adv[t] = last
99
- return adv, adv + vb
100
-
101
-
102
- def ppo_update(ac, opt, data, args):
103
- T, B = data["rew"].shape
104
- obs = data["obs"].reshape(T * B, -1)
105
- act = data["act"].reshape(T * B, ac.act_dim)
106
- old_logp = data["logp"].reshape(T * B)
107
- adv = data["adv"].reshape(T * B)
108
- ret = data["ret"].reshape(T * B)
109
- adv = (adv - adv.mean()) / (adv.std() + 1e-8)
110
- n = obs.shape[0]
111
- stats = dict(pg=[], v=[], ent=[], kl=[], cf=[])
112
- stop = False
113
- ep = 0
114
- for ep in range(args.epochs):
115
- idx = torch.randperm(n)
116
- for s in range(0, n, args.minibatch):
117
- mb = idx[s:s + args.minibatch]
118
- dist = ac.dist(obs[mb])
119
- logp = dist.log_prob(act[mb]).sum(-1)
120
- ratio = (logp - old_logp[mb]).exp()
121
- a = adv[mb]
122
- pg = torch.max(-a * ratio,
123
- -a * torch.clamp(ratio, 1 - args.clip, 1 + args.clip)).mean()
124
- v = ac.value(obs[mb])
125
- v_loss = 0.5 * ((v - ret[mb]) ** 2).mean()
126
- ent = dist.entropy().sum(-1).mean()
127
- loss = pg + args.vf_coef * v_loss - args.ent * ent
128
- opt.zero_grad()
129
- loss.backward()
130
- nn.utils.clip_grad_norm_(ac.parameters(), 0.5)
131
- opt.step()
132
- with torch.no_grad():
133
- kl = ((ratio - 1) - (logp - old_logp[mb])).mean().item()
134
- cf = ((ratio - 1).abs() > args.clip).float().mean().item()
135
- stats["pg"].append(pg.item()); stats["v"].append(v_loss.item())
136
- stats["ent"].append(ent.item()); stats["kl"].append(kl)
137
- stats["cf"].append(cf)
138
- if kl > 2.0 * args.target_kl:
139
- stop = True
140
- break
141
- if stop:
142
- break
143
- out = {k: float(np.mean(v)) for k, v in stats.items()}
144
- out["epochs"] = ep + 1
145
- return out
146
-
147
-
148
- def main():
149
- ap = argparse.ArgumentParser()
150
- ap.add_argument("--exp", type=str, required=True)
151
- ap.add_argument("--seed", type=int, default=0)
152
- ap.add_argument("--init", type=str, default="rand",
153
- choices=["rand", "fixed", "mix"],
154
- help="rand=down noise, fixed=eval state, "
155
- "mix=curriculum with up_frac near upright")
156
- ap.add_argument("--up-frac", type=float, default=0.5,
157
- help="fraction of resets near upright when --init mix")
158
- ap.add_argument("--obs", type=str, default="full",
159
- choices=sorted(OBS_MODES),
160
- help="observation mode (see obs.py)")
161
- ap.add_argument("--reward", type=str, default="base",
162
- choices=["base", "sparse", "energy"],
163
- help="reward preset (see task.py REWARD_PRESETS)")
164
- ap.add_argument("--envs", type=int, default=64)
165
- ap.add_argument("--horizon", type=int, default=600)
166
- ap.add_argument("--minutes", type=float, default=25.0)
167
- ap.add_argument("--iters", type=int, default=10 ** 9)
168
- ap.add_argument("--lr", type=float, default=3e-4)
169
- ap.add_argument("--gamma", type=float, default=0.99)
170
- ap.add_argument("--lam", type=float, default=0.95)
171
- ap.add_argument("--clip", type=float, default=0.2)
172
- ap.add_argument("--epochs", type=int, default=8)
173
- ap.add_argument("--minibatch", type=int, default=2048)
174
- ap.add_argument("--ent", type=float, default=0.003)
175
- ap.add_argument("--vf-coef", type=float, default=0.5)
176
- ap.add_argument("--target-kl", type=float, default=0.03)
177
- ap.add_argument("--hidden", type=int, default=128)
178
- ap.add_argument("--depth", type=int, default=2)
179
- ap.add_argument("--episode-steps", type=int, default=EPISODE_STEPS)
180
- ap.add_argument("--eval-every", type=int, default=5)
181
- ap.add_argument("--tb-root", type=str, default="tb")
182
- ap.add_argument("--ckpt-dir", type=str, default="checkpoints")
183
- ap.add_argument("--out", type=str, default="results")
184
- ap.add_argument("--tau-max", type=float, default=18.0)
185
- ap.add_argument("--damping", type=float, default=0.01)
186
- ap.add_argument("--log-every", type=int, default=1)
187
- ap.add_argument("--robust-starts", type=int, default=8,
188
- help="deterministic extra rollouts from the training "
189
- "reset distribution at the end (0 disables)")
190
- args = ap.parse_args()
191
-
192
- torch.manual_seed(args.seed)
193
- np.random.seed(args.seed)
194
- for d in (args.tb_root, args.ckpt_dir, args.out):
195
- os.makedirs(d, exist_ok=True)
196
- exp = args.exp
197
- writer = SummaryWriter(os.path.join(args.tb_root, exp))
198
- p = Params(tau_max=args.tau_max, b=args.damping)
199
- spec = obs_spec(args.obs)
200
- obs_dim = spec["dim"]
201
- reward_w = get_reward_preset(args.reward)
202
- sim = TorchVecSim(p, n=args.envs, episode_steps=args.episode_steps)
203
- task = TorchTask(p, n=args.envs, reward_w=reward_w, obs_mode=args.obs)
204
- gen = torch.Generator().manual_seed(args.seed * 1000 + 7)
205
-
206
- init_meta = dict(mode=args.init,
207
- training=("theta_i ~ U(-0.05,0.05), omega_i ~ U(-0.02,0.02); no bias"
208
- if args.init == "rand" else
209
- ("theta=[0.013,-0.008,0.021], omega=0 (same as eval)"
210
- if args.init == "fixed" else
211
- f"curriculum: {args.up_frac:.2f} near upright "
212
- f"(pi + U(-0.05,0.05)), rest near down")),
213
- eval="theta=[0.013,-0.008,0.021], omega=0 (fixed, no noise)")
214
- meta = dict(algo="ppo", exp=exp, seed=args.seed, envs=args.envs,
215
- horizon=args.horizon, obs=spec, obs_dim=obs_dim,
216
- init=init_meta, reward=reward_w, reward_preset=args.reward,
217
- reward_note=TORCH_REWARD_NOTE, args=vars(args))
218
- writer.add_text("config", json.dumps(meta, indent=2), 0)
219
- print(json.dumps(meta, indent=2), flush=True)
220
-
221
- ac = ActorCritic(obs_dim, act_dim=2, hidden=args.hidden, depth=args.depth)
222
- opt = torch.optim.Adam(ac.parameters(), lr=args.lr, eps=1e-5)
223
- sched = torch.optim.lr_scheduler.LinearLR(opt, start_factor=1.0,
224
- end_factor=0.2, total_iters=100000)
225
-
226
- th, om = make_init(args.envs, args.init, gen, up_frac=args.up_frac)
227
- theta = sim.reset(th, om)
228
- task.reset(theta)
229
-
230
- t_start = time.time()
231
- global_steps = 0
232
- it = 0
233
- best_t_can = -1.0
234
- best_eval = None
235
- best_path = None
236
- last_path = None
237
- eval_curve = []
238
- last = {}
239
- ep_stats = {k: [] for k in ("return", "t_can", "ever_up", "first_up",
240
- "up_frac", "length")}
241
-
242
- def save_ckpt(path, ev):
243
- torch.save(dict(model=ac.state_dict(), opt=opt.state_dict(),
244
- obs_dim=obs_dim, obs=spec, init=init_meta,
245
- reward=reward_w, reward_preset=args.reward,
246
- args=vars(args),
247
- iter=it, global_steps=global_steps, eval=ev,
248
- algo="ppo"), os.path.join(args.ckpt_dir, path))
249
-
250
- def log_eval(ev, tag=""):
251
- nonlocal best_t_can, best_eval, best_path
252
- eval_curve.append(dict(iter=it, global_steps=global_steps, **ev))
253
- for k, v in ev.items():
254
- if isinstance(v, (int, float)):
255
- writer.add_scalar(f"eval/{k}", v, global_steps)
256
- if (ev["t_can"] > best_t_can
257
- or (ev["t_can"] == best_t_can and best_eval is not None
258
- and ev["ep_return"] > best_eval["ep_return"])):
259
- best_t_can = ev["t_can"]
260
- best_eval = dict(ev)
261
- best_path = exp + "_best.pt"
262
- save_ckpt(best_path, ev)
263
-
264
- while it < args.iters:
265
- if (time.time() - t_start) / 60.0 > args.minutes:
266
- print("time budget reached", flush=True)
267
- break
268
- data = collect(sim, task, ac, args.horizon, args, gen, ep_stats)
269
- adv, ret = gae(data["rew"], data["val"], data["done"], data["last_v"],
270
- args.gamma, args.lam)
271
- data["adv"], data["ret"] = adv, ret
272
- stats = ppo_update(ac, opt, data, args)
273
- sched.step()
274
- global_steps += args.horizon * args.envs
275
- it += 1
276
- last = stats
277
- mean_train_ret = (float(np.mean(ep_stats["return"]))
278
- if ep_stats["return"] else float("nan"))
279
-
280
- if ep_stats["return"]:
281
- writer.add_scalar("train/episode_return",
282
- float(np.mean(ep_stats["return"])), global_steps)
283
- writer.add_scalar("train/episode_length",
284
- float(np.mean(ep_stats["length"])), global_steps)
285
- writer.add_scalar("train/t_can", float(np.mean(ep_stats["t_can"])),
286
- global_steps)
287
- writer.add_scalar("train/t_can_max", float(np.max(ep_stats["t_can"])),
288
- global_steps)
289
- writer.add_scalar("train/success_rate",
290
- float(np.mean(np.asarray(ep_stats["t_can"]) >= 10.0)),
291
- global_steps)
292
- writer.add_scalar("train/ever_up_frac",
293
- float(np.mean(ep_stats["ever_up"])), global_steps)
294
- writer.add_scalar("train/up_frac",
295
- float(np.mean(ep_stats["up_frac"])), global_steps)
296
- fu = [x for x in ep_stats["first_up"] if x > 0]
297
- if fu:
298
- writer.add_scalar("train/first_up_time", float(np.mean(fu)),
299
- global_steps)
300
- ep_stats = {k: [] for k in ep_stats}
301
- writer.add_scalar("train/mean_step_reward", float(data["rew"].mean()), global_steps)
302
- writer.add_scalar("train/action_abs_mean", float(data["act"].abs().mean()), global_steps)
303
- writer.add_scalar("train/action_std", float(data["act"].std()), global_steps)
304
- writer.add_scalar("train/std", float(ac.effective_log_std().exp().mean()),
305
- global_steps)
306
- writer.add_scalar("train/lr", opt.param_groups[0]["lr"], global_steps)
307
- writer.add_scalar("train/approx_kl", stats["kl"], global_steps)
308
- writer.add_scalar("train/clip_frac", stats["cf"], global_steps)
309
- writer.add_scalar("train/global_steps", global_steps, global_steps)
310
- writer.add_scalar("loss/policy", stats["pg"], global_steps)
311
- writer.add_scalar("loss/value", stats["v"], global_steps)
312
- writer.add_scalar("loss/entropy", stats["ent"], global_steps)
313
- writer.add_scalar("time/elapsed_min", (time.time() - t_start) / 60.0,
314
- global_steps)
315
-
316
- if it % args.eval_every == 0 or it == 1:
317
- ev = evaluate(make_ppo_policy(ac.actor), p, args.episode_steps,
318
- reward_w=reward_w, obs_mode=args.obs)
319
- log_eval(ev)
320
- last_path = exp + "_last.pt"
321
- save_ckpt(last_path, ev)
322
- print(f"iter {it:4d} steps {global_steps/1e6:5.2f}M "
323
- f"train_ret {mean_train_ret:8.1f} "
324
- f"EVAL tc {ev['t_can']:6.2f} succ {ev['success']:.0f} "
325
- f"everup {ev['ever_up']:.0f} angerr {ev['final_ang_err']:.3f} "
326
- f"ret {ev['ep_return']:8.1f} pi {stats['pg']:+.3f} "
327
- f"v {stats['v']:.2f} ent {stats['ent']:.2f} kl {stats['kl']:.3f} "
328
- f"({time.time()-t_start:.0f}s)", flush=True)
329
- elif it % args.log_every == 0:
330
- print(f"iter {it:4d} steps {global_steps/1e6:5.2f}M "
331
- f"train_ret {mean_train_ret:8.1f} "
332
- f"pi {stats['pg']:+.3f} v {stats['v']:.2f} ent {stats['ent']:.2f} "
333
- f"kl {stats['kl']:.3f} ({time.time()-t_start:.0f}s)", flush=True)
334
- writer.flush()
335
-
336
- ev_final = evaluate(make_ppo_policy(ac.actor), p, args.episode_steps,
337
- reward_w=reward_w, obs_mode=args.obs)
338
- eval_curve.append(dict(iter=it, global_steps=global_steps, **ev_final))
339
- for k, v in ev_final.items():
340
- if isinstance(v, (int, float)):
341
- writer.add_scalar(f"eval_final/{k}", v, global_steps)
342
- final_path = exp + "_final.pt"
343
- save_ckpt(final_path, ev_final)
344
- last_path = exp + "_last.pt"
345
- save_ckpt(last_path, ev_final)
346
-
347
- # Extra deterministic rollouts from the training reset distribution. This
348
- # is NOT the mandated fixed-state eval; it checks generalisation.
349
- rob = None
350
- rob_best = None
351
- if args.robust_starts > 0:
352
- g = torch.Generator().manual_seed(args.seed * 1000 + 123)
353
- th_r, om_r = sample_train_init(args.robust_starts, gen=g)
354
- starts = list(zip(th_r.numpy(), om_r.numpy()))
355
- rob = evaluate_starts(make_ppo_policy(ac.actor), p, starts,
356
- args.episode_steps, reward_w=reward_w,
357
- obs_mode=args.obs)
358
- for k, v in rob.items():
359
- if isinstance(v, (int, float)):
360
- writer.add_scalar(f"robust/{k}", v, global_steps)
361
- if best_path is not None:
362
- ck = torch.load(os.path.join(args.ckpt_dir, best_path),
363
- map_location="cpu")
364
- ac_best = ActorCritic(obs_dim, act_dim=2, hidden=args.hidden,
365
- depth=args.depth)
366
- ac_best.load_state_dict(ck["model"])
367
- rob_best = evaluate_starts(make_ppo_policy(ac_best.actor), p, starts,
368
- args.episode_steps, reward_w=reward_w,
369
- obs_mode=args.obs)
370
- for k, v in rob_best.items():
371
- if isinstance(v, (int, float)):
372
- writer.add_scalar(f"robust_best/{k}", v, global_steps)
373
-
374
- wall = time.time() - t_start
375
- out = dict(algo="ppo", exp=exp, seed=args.seed, envs=args.envs,
376
- horizon=args.horizon, obs=spec, obs_dim=obs_dim,
377
- init=init_meta, reward=reward_w, reward_preset=args.reward,
378
- reward_note=TORCH_REWARD_NOTE, args=vars(args),
379
- minutes_budget=args.minutes, global_steps=global_steps,
380
- iters=it, wall_time_s=wall,
381
- steps_per_s=global_steps / max(wall, 1e-9),
382
- final_eval=ev_final, best_eval=best_eval,
383
- best_t_can=best_t_can, eval_curve=eval_curve,
384
- robust=rob, robust_best=rob_best,
385
- final_train=dict(policy_loss=last.get("pg"),
386
- value_loss=last.get("v"),
387
- entropy=last.get("ent"),
388
- approx_kl=last.get("kl")),
389
- checkpoints=dict(best=best_path, last=last_path,
390
- final=final_path))
391
- with open(os.path.join(args.out, exp + ".json"), "w") as f:
392
- json.dump(out, f, indent=2)
393
- writer.flush()
394
- writer.close()
395
- print("DONE " + json.dumps(dict(
396
- exp=exp, init=args.init, seed=args.seed, global_steps=global_steps,
397
- wall_s=round(wall, 1), steps_per_s=round(out["steps_per_s"], 1),
398
- eval_t_can=ev_final["t_can"], best_t_can=best_t_can,
399
- eval_return=ev_final["ep_return"])), flush=True)
400
-
401
-
402
- if __name__ == "__main__":
403
- main()