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,39 @@
1
+ # Fixed-env experiment table (auto-generated)
2
+
3
+ | exp | algo | init | obs | dim | reward | steps | wall min | steps/s | final t_can | best t_can | final succ | final ever_up | final ret | final err | first succ @ steps | final robust succ | final robust t_can | best robust succ | best robust t_can |
4
+ |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
5
+ | ppo_angle_base_rand | ppo | rand | angle | 24 | base | 31.76M | 40.6 | 13047 | 0.04 | 0.97 | 0 | 1 | 2697.0 | 0.9751 | - | 0.00 | 0.06 | 0.00 | 0.24 |
6
+ | ppo_angle_h1_rand | ppo | rand | angle_h1 | 6 | base | 20.97M | 30.4 | 11493 | 0.00 | 0.00 | 0 | 0 | -3526.7 | 2.9346 | - | 0.00 | 0.00 | 0.00 | 0.00 |
7
+ | ppo_angle_h8_rand | ppo | rand | angle_h8 | 48 | base | 30.11M | 40.4 | 12417 | 0.05 | 1.31 | 0 | 1 | 2629.5 | 0.8636 | - | 0.00 | 0.05 | 0.00 | 0.15 |
8
+ | ppo_angle_mix | ppo | mix | angle | 24 | base | 23.50M | 30.5 | 12847 | 0.00 | 0.25 | 0 | 1 | -13092.4 | 2.8925 | - | 0.00 | 0.00 | 0.00 | 0.10 |
9
+ | ppo_full_base_fixed | ppo | fixed | full | 36 | base | 21.93M | 30.5 | 11978 | 0.00 | 1.19 | 0 | 0 | 260.7 | 0.7154 | - | 0.00 | 0.00 | 0.00 | 1.03 |
10
+ | ppo_full_base_rand | ppo | rand | full | 36 | base | 35.98M | 45.1 | 13307 | 26.61 | 26.85 | 1 | 1 | 16772.5 | 0.0220 | 11.33M | 1.00 | 25.57 | 1.00 | 26.86 |
11
+ | ppo_full_base_rand_s1 | ppo | rand | full | 36 | base | 29.03M | 40.8 | 11872 | 0.23 | 27.17 | 0 | 1 | 5034.6 | 0.2765 | 13.63M | 0.00 | 0.21 | 1.00 | 26.78 |
12
+ | ppo_full_base_rand_s2 | ppo | rand | full | 36 | base | 34.21M | 40.3 | 14135 | 0.00 | 28.46 | 0 | 0 | 4804.4 | 0.0820 | 14.40M | 0.00 | 0.00 | 1.00 | 28.03 |
13
+ | ppo_full_energy_rand | ppo | rand | full | 36 | energy | 24.50M | 35.5 | 11503 | 0.00 | 0.00 | 0 | 0 | -1948.3 | 2.8125 | - | 0.00 | 0.00 | 0.00 | 0.00 |
14
+ | ppo_full_h1_mix | ppo | mix | full_h1 | 9 | base | 21.39M | 30.5 | 11703 | 0.00 | 0.02 | 0 | 0 | -552.6 | 2.2633 | - | 0.00 | 0.00 | 0.00 | 0.00 |
15
+ | ppo_full_h1_rand | ppo | rand | full_h1 | 9 | base | 25.42M | 35.3 | 11988 | 0.00 | 0.06 | 0 | 0 | -1701.9 | 2.6674 | - | 0.00 | 0.00 | 0.00 | 0.06 |
16
+ | ppo_full_sparse_rand | ppo | rand | full | 36 | sparse | 19.12M | 30.5 | 10445 | 0.00 | 0.00 | 0 | 0 | -0.4 | 3.1021 | - | 0.00 | 0.00 | 0.00 | 0.00 |
17
+ | sac_angle_base_rand | sac | rand | angle | 24 | base | 1.42M | 50.6 | 469 | 0.00 | 0.04 | 0 | 0 | -581.4 | 0.6124 | - | 0.00 | 0.01 | 0.00 | 0.00 |
18
+ | sac_full_base_fixed | sac | fixed | full | 36 | base | 1.41M | 50.4 | 466 | 27.62 | 28.86 | 1 | 1 | 17697.1 | 0.0324 | 0.52M | 1.00 | 27.49 | 1.00 | 23.45 |
19
+ | sac_full_base_rand | sac | rand | full | 36 | base | 1.42M | 50.6 | 470 | 28.21 | 28.75 | 1 | 1 | 17607.2 | 0.0293 | 0.70M | 1.00 | 26.39 | 0.75 | 20.89 |
20
+
21
+ ## TensorBoard coverage
22
+
23
+ | exp | tags | required missing | last global_steps | last elapsed min |
24
+ |---|---|---|---|---|
25
+ | ppo_angle_base_rand | 62 | none | 31756800 | 40.04 |
26
+ | ppo_angle_h1_rand | 62 | none | 20966400 | 30.02 |
27
+ | ppo_angle_h8_rand | 62 | none | 30105600 | 40.04 |
28
+ | ppo_angle_mix | 62 | none | 23500800 | 30.02 |
29
+ | ppo_full_base_fixed | 62 | none | 21926400 | 30.01 |
30
+ | ppo_full_base_rand | 53 | none | 35980800 | 45.06 |
31
+ | ppo_full_base_rand_s1 | 62 | none | 29030400 | 40.02 |
32
+ | ppo_full_base_rand_s2 | 62 | none | 34214400 | 40.02 |
33
+ | ppo_full_energy_rand | 62 | none | 24499200 | 35.02 |
34
+ | ppo_full_h1_mix | 62 | none | 21388800 | 30.04 |
35
+ | ppo_full_h1_rand | 62 | none | 25420800 | 35.00 |
36
+ | ppo_full_sparse_rand | 61 | none | 19123200 | 30.05 |
37
+ | sac_angle_base_rand | 66 | none | 1423040 | 49.99 |
38
+ | sac_full_base_fixed | 66 | none | 1407680 | 49.99 |
39
+ | sac_full_base_rand | 66 | none | 1424320 | 50.00 |
@@ -0,0 +1,324 @@
1
+ """3-link planar pendulum simulator with a strict angle-only API.
2
+
3
+ Physical model (verified in the sibling rounds):
4
+ * 3 links, joints 1 and 3 actuated, joint 2 passive.
5
+ * tau_max = 18 N m at each actuated joint, action u in [-1, 1]^2.
6
+ * 100 Hz zero-order-hold control, RK4 with 4 substeps (dt = 2.5 ms).
7
+ * 30 s episode = 3000 control steps.
8
+ * theta_i is the absolute link angle from the DOWNWARD vertical
9
+ (0 = down, +-pi = up); angles are NOT divided by pi anywhere.
10
+
11
+ Environment contract (deliberate, so that no training signal lives in the
12
+ simulator):
13
+ reset(theta, omega) -> theta (only the three angles)
14
+ step(u) -> theta (only the three angles)
15
+ There is no reward, loss, done, info, terminal flag, tip or omega in any
16
+ return value. A public theta property exists only for diagnostics and
17
+ returns the same three angles.
18
+ An episode is exactly episode_steps (default 3000) control steps.
19
+ Calling step() when any environment in the batch has already used its
20
+ budget raises EpisodeLengthError; reset() must be called first.
21
+
22
+ The internal omega is advanced by the physics and is deliberately not part
23
+ of the public API. Training code that wants a velocity signal must derive it
24
+ from the returned angle stream (see obs.py).
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import numpy as np
29
+
30
+ N_LINKS = 3
31
+ DRIVE_JOINTS = (0, 2) # zero-based: joint 1 and joint 3 actuated
32
+ DT = 0.01 # control period (100 Hz)
33
+ N_ACT = 2
34
+ EPISODE_STEPS = 3000 # 30 s
35
+ UP_TOL = 0.05 # success tolerance (rad)
36
+ BONUS_TOL = 0.15 # reward bonus tolerance (rad)
37
+
38
+
39
+ class Params:
40
+ """Physical parameters of the 3-link pendulum (defaults = task spec)."""
41
+
42
+ def __init__(self, m=(1.0, 1.0, 1.0), l=(0.5, 0.5, 0.5), g=9.81, b=0.01,
43
+ tau_max=18.0, r=None, I=None, drive=DRIVE_JOINTS):
44
+ self.m = np.asarray(m, dtype=np.float64)
45
+ self.l = np.asarray(l, dtype=np.float64)
46
+ self.g = float(g)
47
+ self.b = float(b)
48
+ self.tau_max = float(tau_max)
49
+ self.drive = tuple(drive)
50
+ self.r = np.asarray(r, dtype=np.float64) if r is not None else 0.5 * self.l
51
+ self.I = (np.asarray(I, dtype=np.float64) if I is not None
52
+ else self.m * self.l ** 2 / 12.0)
53
+
54
+ n = N_LINKS
55
+ self.gvec = np.array([self.m[i] * self.r[i] + self.l[i] * self.m[i + 1:].sum()
56
+ for i in range(n)])
57
+
58
+ self.A = np.zeros((n, n))
59
+ for i in range(n):
60
+ for j in range(n):
61
+ if i == j:
62
+ self.A[i, i] = (self.I[i] + self.m[i] * self.r[i] ** 2
63
+ + self.l[i] ** 2 * self.m[i + 1:].sum())
64
+ else:
65
+ a, bb = min(i, j), max(i, j)
66
+ self.A[i, j] = self.l[a] * self.gvec[bb]
67
+
68
+ self.vcoef = self.gvec.copy()
69
+
70
+ # Rayleigh damping matrix D^T D, eps_i = omega_i - omega_{i-1}
71
+ D = np.zeros((n, n))
72
+ for i in range(n):
73
+ D[i, i] = 1.0
74
+ if i > 0:
75
+ D[i, i - 1] = -1.0
76
+ self.Ldamp = D.T @ D
77
+ self.L_total = float(self.l.sum())
78
+
79
+ def tip_position(self, theta):
80
+ return float(np.dot(self.l, np.sin(theta))), float(-np.dot(self.l, np.cos(theta)))
81
+
82
+ def potential(self, theta):
83
+ return -self.g * float(np.dot(self.vcoef, np.cos(theta)))
84
+
85
+ def as_dict(self):
86
+ return dict(m=self.m.tolist(), l=self.l.tolist(), r=self.r.tolist(),
87
+ I=self.I.tolist(), g=self.g, b=self.b,
88
+ tau_max=self.tau_max, drive=list(self.drive))
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # numpy float64 physics
93
+ # ---------------------------------------------------------------------------
94
+ def generalized_torque(u, p: Params):
95
+ """Physical joint-motor torques mapped to absolute-angle generalized forces.
96
+
97
+ Joint 1 is external to ground, so its generalized force is +u1*tau_max.
98
+ Joint 3 is an internal motor between links 2 and 3: its torque acts on the
99
+ relative angle theta3-theta2, giving generalized force
100
+ (tau1, -tau3, tau3)
101
+ with tau2=0. Do NOT use (tau1, 0, tau3): that would be a reactionless
102
+ external torque on link 3 and contradicts joint 2 being passive.
103
+ """
104
+ tau = np.zeros(N_LINKS)
105
+ u = np.asarray(u, dtype=np.float64)
106
+ # Fixed mapping for this task: drive=(0,2), tau2=0.
107
+ tau[0] = float(u[0]) * p.tau_max
108
+ tau[1] = -float(u[1]) * p.tau_max
109
+ tau[2] = float(u[1]) * p.tau_max
110
+ return tau
111
+
112
+
113
+ def rhs(theta, omega, u, p: Params):
114
+ d = theta[:, None] - theta[None, :]
115
+ M = p.A * np.cos(d)
116
+ C = (p.A * np.sin(d) * (omega[None, :] ** 2)).sum(axis=1)
117
+ G = p.g * np.sin(theta) * p.gvec
118
+ tau = generalized_torque(u, p) - p.b * (p.Ldamp @ omega)
119
+ qdd = np.linalg.solve(M, tau - C - G)
120
+ return omega, qdd
121
+
122
+
123
+ def rk4_step(theta, omega, u, p: Params, dt):
124
+ k1t, k1w = rhs(theta, omega, u, p)
125
+ k2t, k2w = rhs(theta + 0.5 * dt * k1t, omega + 0.5 * dt * k1w, u, p)
126
+ k3t, k3w = rhs(theta + 0.5 * dt * k2t, omega + 0.5 * dt * k2w, u, p)
127
+ k4t, k4w = rhs(theta + dt * k3t, omega + dt * k3w, u, p)
128
+ theta = theta + (dt / 6.0) * (k1t + 2 * k2t + 2 * k3t + k4t)
129
+ omega = omega + (dt / 6.0) * (k1w + 2 * k2w + 2 * k3w + k4w)
130
+ return theta, omega
131
+
132
+
133
+ def integrate(theta, omega, u, p: Params, dt_ctrl=DT, n_sub=4):
134
+ h = dt_ctrl / n_sub
135
+ for _ in range(n_sub):
136
+ theta, omega = rk4_step(theta, omega, u, p, h)
137
+ return theta, omega
138
+
139
+
140
+ def total_energy(theta, omega, p: Params):
141
+ d = theta[:, None] - theta[None, :]
142
+ M = p.A * np.cos(d)
143
+ T = 0.5 * float(omega @ M @ omega)
144
+ V = -p.g * float(np.dot(p.vcoef, np.cos(theta)))
145
+ return T + V, T, V
146
+
147
+
148
+ def energy_up(p: Params):
149
+ return p.g * float(np.dot(p.vcoef, np.ones(N_LINKS)))
150
+
151
+
152
+ def energy_down(p: Params):
153
+ return -p.g * float(np.dot(p.vcoef, np.ones(N_LINKS)))
154
+
155
+
156
+ def wrap_to_pi(a):
157
+ return (a + np.pi) % (2.0 * np.pi) - np.pi
158
+
159
+
160
+ def upright_error(theta):
161
+ return wrap_to_pi(theta - np.pi)
162
+
163
+
164
+ def tip_pos(theta, p: Params):
165
+ return p.tip_position(theta)
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # batched torch float32 physics (training)
170
+ # ---------------------------------------------------------------------------
171
+ def make_torch_consts(p: Params, device="cpu"):
172
+ import torch
173
+ # u @ tau_map must produce the absolute generalized force (tau1,-tau3,tau3).
174
+ tau_map = torch.zeros(2, N_LINKS, dtype=torch.float32, device=device)
175
+ tau_map[0, 0] = 1.0 # joint 1: +tau1
176
+ tau_map[1, 1] = -1.0 # joint 3 reaction on link 2: -tau3
177
+ tau_map[1, 2] = 1.0 # joint 3 on link 3: +tau3
178
+ return dict(
179
+ A=torch.tensor(p.A, dtype=torch.float32, device=device),
180
+ gvec=torch.tensor(p.gvec, dtype=torch.float32, device=device),
181
+ vcoef=torch.tensor(p.vcoef, dtype=torch.float32, device=device),
182
+ Ldamp=torch.tensor(p.Ldamp, dtype=torch.float32, device=device),
183
+ lt=torch.tensor(p.l, dtype=torch.float32, device=device),
184
+ tau_map=tau_map,
185
+ g=p.g, b=p.b, tau_max=p.tau_max,
186
+ )
187
+
188
+
189
+ def torch_rhs(theta, omega, u, c):
190
+ import torch
191
+ d = theta.unsqueeze(2) - theta.unsqueeze(1)
192
+ M = c["A"] * torch.cos(d)
193
+ C = (c["A"] * torch.sin(d) * omega.unsqueeze(1) ** 2).sum(dim=2)
194
+ G = c["g"] * torch.sin(theta) * c["gvec"]
195
+ tau = u @ c["tau_map"] * c["tau_max"]
196
+ tau = tau - c["b"] * (omega @ c["Ldamp"].t())
197
+ qdd = torch.linalg.solve(M, (tau - C - G).unsqueeze(-1)).squeeze(-1)
198
+ return omega, qdd
199
+
200
+
201
+ def torch_rk4_step(theta, omega, u, c, dt):
202
+ k1t, k1w = torch_rhs(theta, omega, u, c)
203
+ k2t, k2w = torch_rhs(theta + 0.5 * dt * k1t, omega + 0.5 * dt * k1w, u, c)
204
+ k3t, k3w = torch_rhs(theta + 0.5 * dt * k2t, omega + 0.5 * dt * k2w, u, c)
205
+ k4t, k4w = torch_rhs(theta + dt * k3t, omega + dt * k3w, u, c)
206
+ theta = theta + (dt / 6.0) * (k1t + 2 * k2t + 2 * k3t + k4t)
207
+ omega = omega + (dt / 6.0) * (k1w + 2 * k2w + 2 * k3w + k4w)
208
+ return theta, omega
209
+
210
+
211
+ def torch_integrate(theta, omega, u, c, dt_ctrl=DT, n_sub=4):
212
+ h = dt_ctrl / n_sub
213
+ for _ in range(n_sub):
214
+ theta, omega = torch_rk4_step(theta, omega, u, c, h)
215
+ return theta, omega
216
+
217
+
218
+ def torch_tip(theta, c):
219
+ import torch
220
+ x = (torch.sin(theta) * c["lt"]).sum(-1)
221
+ y = -(torch.cos(theta) * c["lt"]).sum(-1)
222
+ return torch.stack([x, y], dim=-1)
223
+
224
+
225
+ # ---------------------------------------------------------------------------
226
+ # public simulator API
227
+ # ---------------------------------------------------------------------------
228
+ class EpisodeLengthError(RuntimeError):
229
+ """Raised when step() is called after the 3000-step budget is used up."""
230
+
231
+
232
+ class PendulumSim:
233
+ """Single float64 simulator. Returns angles only."""
234
+
235
+ def __init__(self, p=None, episode_steps: int = EPISODE_STEPS,
236
+ dt: float = DT, n_sub: int = 4):
237
+ self.p = Params() if p is None else p
238
+ self.episode_steps = int(episode_steps)
239
+ self.dt = float(dt)
240
+ self.n_sub = int(n_sub)
241
+ self._theta = np.zeros(N_LINKS)
242
+ self._omega = np.zeros(N_LINKS)
243
+ self._steps = 0
244
+
245
+ @property
246
+ def steps(self):
247
+ return self._steps
248
+
249
+ @property
250
+ def theta(self):
251
+ return self._theta.copy()
252
+
253
+ def reset(self, theta, omega):
254
+ theta = np.asarray(theta, dtype=np.float64).reshape(N_LINKS)
255
+ omega = np.asarray(omega, dtype=np.float64).reshape(N_LINKS)
256
+ if not (np.isfinite(theta).all() and np.isfinite(omega).all()):
257
+ raise ValueError("non-finite initial state")
258
+ self._theta = theta.copy()
259
+ self._omega = omega.copy()
260
+ self._steps = 0
261
+ return self._theta.copy()
262
+
263
+ def step(self, u):
264
+ if self._steps >= self.episode_steps:
265
+ raise EpisodeLengthError(
266
+ f"episode budget exhausted ({self.episode_steps} steps); call reset()")
267
+ u = np.clip(np.asarray(u, dtype=np.float64).reshape(N_ACT), -1.0, 1.0)
268
+ self._theta, self._omega = integrate(self._theta, self._omega, u,
269
+ self.p, self.dt, self.n_sub)
270
+ self._steps += 1
271
+ return self._theta.copy()
272
+
273
+
274
+ class TorchVecSim:
275
+ """Batched float32 simulator. Returns angles only; no reward/done."""
276
+
277
+ def __init__(self, p: Params, n: int, episode_steps: int = EPISODE_STEPS,
278
+ device="cpu", dt: float = DT, n_sub: int = 4):
279
+ import torch
280
+ self.torch = torch
281
+ self.p = p
282
+ self.n = int(n)
283
+ self.episode_steps = int(episode_steps)
284
+ self.device = device
285
+ self.dt = float(dt)
286
+ self.n_sub = int(n_sub)
287
+ self.c = make_torch_consts(p, device)
288
+ self.theta = torch.zeros(n, N_LINKS, device=device)
289
+ self._omega = torch.zeros(n, N_LINKS, device=device)
290
+ self.steps = torch.zeros(n, dtype=torch.long, device=device)
291
+
292
+ def reset(self, theta, omega, idx=None):
293
+ t = self.torch
294
+ theta = t.as_tensor(theta, dtype=t.float32, device=self.device).reshape(-1, N_LINKS)
295
+ omega = t.as_tensor(omega, dtype=t.float32, device=self.device).reshape(-1, N_LINKS)
296
+ if idx is None:
297
+ if theta.shape[0] != self.n:
298
+ raise ValueError(f"expected {self.n} initial states, got {theta.shape[0]}")
299
+ self.theta = theta.clone()
300
+ self._omega = omega.clone()
301
+ self.steps = t.zeros(self.n, dtype=t.long, device=self.device)
302
+ return self.theta.clone()
303
+ idx = t.as_tensor(idx, dtype=t.long, device=self.device).flatten()
304
+ if theta.shape[0] != idx.numel():
305
+ raise ValueError("theta subset size does not match idx")
306
+ self.theta[idx] = theta
307
+ self._omega[idx] = omega
308
+ self.steps[idx] = 0
309
+ return self.theta[idx].clone()
310
+
311
+ def step(self, u):
312
+ t = self.torch
313
+ exhausted = self.steps >= self.episode_steps
314
+ if bool(exhausted.any()):
315
+ bad = exhausted.nonzero().flatten().tolist()
316
+ raise EpisodeLengthError(
317
+ "episode budget exhausted for envs "
318
+ f"{bad[:8]}{'...' if len(bad) > 8 else ''} "
319
+ f"({self.episode_steps} steps); call reset() first")
320
+ u = t.clamp(t.as_tensor(u, dtype=t.float32, device=self.device), -1.0, 1.0)
321
+ self.theta, self._omega = torch_integrate(self.theta, self._omega, u,
322
+ self.c, self.dt, self.n_sub)
323
+ self.steps = self.steps + 1
324
+ return self.theta.clone()
@@ -0,0 +1,175 @@
1
+ """Aggregate results/*.json and tb/<exp> into results/summary.json,
2
+ results/table.md and results/curves.png. Missing runs simply do not appear.
3
+
4
+ Usage: python3 summarize.py [--tb-root tb] [--out results]
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+
14
+ import matplotlib
15
+ matplotlib.use("Agg")
16
+ import matplotlib.pyplot as plt
17
+
18
+ ROOT = Path(__file__).resolve().parent
19
+ REQUIRED_TAGS = ["train/episode_return", "train/episode_length", "train/t_can",
20
+ "train/success_rate", "eval/t_can", "eval/success",
21
+ "eval/final_ang_err", "time/elapsed_min"]
22
+
23
+
24
+ def tb_status(tb_root, exp):
25
+ from tensorboard.backend.event_processing.event_accumulator import (
26
+ EventAccumulator)
27
+ d = Path(tb_root) / exp
28
+ if not d.is_dir():
29
+ return dict(exp=exp, exists=False, tags=[], missing=REQUIRED_TAGS,
30
+ last=dict())
31
+ ea = EventAccumulator(str(d), size_guidance={"scalars": 0})
32
+ ea.Reload()
33
+ tags = ea.Tags().get("scalars", [])
34
+ last = {}
35
+ for k in ("train/global_steps", "time/elapsed_min", "train/t_can",
36
+ "train/success_rate", "eval/t_can", "eval/success"):
37
+ if k in tags:
38
+ last[k] = ea.Scalars(k)[-1].value
39
+ return dict(exp=exp, exists=True, tags=sorted(tags),
40
+ missing=[t for t in REQUIRED_TAGS if t not in tags],
41
+ n_points=len(tags), last=last)
42
+
43
+
44
+ def load_results(res_dir):
45
+ out = []
46
+ for path in sorted(Path(res_dir).glob("*.json")):
47
+ if path.name in ("summary.json", "tb_coverage.json", "robustness.json",
48
+ "reference_controller.json"):
49
+ continue
50
+ with open(path) as f:
51
+ out.append(json.load(f))
52
+ return out
53
+
54
+
55
+ def fmt(x, nd=2):
56
+ if x is None:
57
+ return "-"
58
+ if isinstance(x, float):
59
+ if not np.isfinite(x):
60
+ return "nan"
61
+ return f"{x:.{nd}f}"
62
+ return str(x)
63
+
64
+
65
+ def main():
66
+ ap = argparse.ArgumentParser()
67
+ ap.add_argument("--tb-root", default="tb")
68
+ ap.add_argument("--out", default="results")
69
+ args = ap.parse_args()
70
+ results = load_results(args.out)
71
+ rows, cov = [], []
72
+ for r in results:
73
+ ev = r.get("final_eval") or {}
74
+ best = r.get("best_eval") or {}
75
+ ck = r.get("checkpoints") or {}
76
+ obs = r.get("obs") or {}
77
+ init = r.get("init") or {}
78
+ init_mode = init.get("mode") if isinstance(init, dict) else str(init)
79
+ curve = r.get("eval_curve") or []
80
+ first_succ = next((c["global_steps"] for c in curve
81
+ if c.get("t_can", 0.0) >= 10.0), None)
82
+ rob = r.get("robust") or {}
83
+ rob_best = r.get("robust_best") or {}
84
+ c = tb_status(args.tb_root, r["exp"])
85
+ cov.append(c)
86
+ rows.append(dict(
87
+ exp=r["exp"], algo=r.get("algo"), init=init_mode,
88
+ obs=obs.get("mode", "-"), obs_dim=r.get("obs_dim"),
89
+ reward=r.get("reward_preset", "-"),
90
+ envs=r.get("envs"), global_steps=r.get("global_steps"),
91
+ wall_min=(r.get("wall_time_s") or 0.0) / 60.0,
92
+ steps_per_s=r.get("steps_per_s"),
93
+ final_t_can=ev.get("t_can"), best_t_can=best.get("t_can"),
94
+ final_success=ev.get("success"), final_ever_up=ev.get("ever_up"),
95
+ final_return=ev.get("ep_return"), best_return=best.get("ep_return"),
96
+ final_ang_err=ev.get("final_ang_err"),
97
+ first_success_steps=first_succ,
98
+ robust_success=rob.get("success_rate"),
99
+ robust_mean_t_can=rob.get("mean_t_can"),
100
+ robust_best_success=rob_best.get("success_rate"),
101
+ robust_best_mean_t_can=rob_best.get("mean_t_can"),
102
+ best_ckpt=ck.get("best"), final_ckpt=ck.get("final"),
103
+ tb_tags=len(c.get("tags", [])), tb_missing=c.get("missing", []),
104
+ ))
105
+
106
+ table = ["# Fixed-env experiment table (auto-generated)",
107
+ "",
108
+ "| exp | algo | init | obs | dim | reward | steps | wall min | "
109
+ "steps/s | final t_can | best t_can | final succ | final ever_up | "
110
+ "final ret | final err | first succ @ steps | final robust succ | "
111
+ "final robust t_can | best robust succ | best robust t_can |",
112
+ "|" + "---|" * 20]
113
+ for x in rows:
114
+ fs = (f"{x['first_success_steps']/1e6:.2f}M"
115
+ if x["first_success_steps"] else "-")
116
+ table.append(
117
+ f"| {x['exp']} | {x['algo']} | {x['init']} | {x['obs']} | "
118
+ f"{x['obs_dim']} | {x['reward']} | "
119
+ f"{(x['global_steps'] or 0)/1e6:.2f}M | {x['wall_min']:.1f} | "
120
+ f"{fmt(x['steps_per_s'],0)} | {fmt(x['final_t_can'])} | "
121
+ f"{fmt(x['best_t_can'])} | {fmt(x['final_success'],0)} | "
122
+ f"{fmt(x['final_ever_up'],0)} | {fmt(x['final_return'],1)} | "
123
+ f"{fmt(x['final_ang_err'],4)} | {fs} | "
124
+ f"{fmt(x['robust_success'],2)} | {fmt(x['robust_mean_t_can'])} | "
125
+ f"{fmt(x['robust_best_success'],2)} | "
126
+ f"{fmt(x['robust_best_mean_t_can'])} |")
127
+ table += ["", "## TensorBoard coverage", "",
128
+ "| exp | tags | required missing | last global_steps | last elapsed min |",
129
+ "|---|---|---|---|---|"]
130
+ for c in cov:
131
+ last = c.get("last") or {}
132
+ table.append(f"| {c['exp']} | {len(c.get('tags', []))} | "
133
+ f"{', '.join(c.get('missing', [])) or 'none'} | "
134
+ f"{fmt(last.get('train/global_steps'),0)} | "
135
+ f"{fmt(last.get('time/elapsed_min'))} |")
136
+ Path(args.out, "table.md").write_text("\n".join(table) + "\n")
137
+
138
+ if results:
139
+ fig, axes = plt.subplots(1, 2, figsize=(13, 4.6))
140
+ for r in results:
141
+ curve = r.get("eval_curve") or []
142
+ if not curve:
143
+ continue
144
+ steps = [c["global_steps"] / 1e6 for c in curve]
145
+ tcan = [c["t_can"] for c in curve]
146
+ ret = [c["ep_return"] for c in curve]
147
+ lab = r["exp"]
148
+ axes[0].plot(steps, tcan, lw=1.2, label=lab)
149
+ axes[1].plot(steps, ret, lw=1.2, label=lab)
150
+ axes[0].axhline(10.0, color="k", ls=":", lw=1)
151
+ axes[0].set_xlabel("env steps (M)")
152
+ axes[0].set_ylabel("eval t_can (s)")
153
+ axes[0].set_title("fixed-initial deterministic eval: t_can")
154
+ axes[0].grid(alpha=0.3)
155
+ axes[1].set_xlabel("env steps (M)")
156
+ axes[1].set_ylabel("eval return (symlog)")
157
+ axes[1].set_title("fixed-initial deterministic eval: return")
158
+ axes[1].set_yscale("symlog", linthresh=1000.0)
159
+ axes[1].grid(alpha=0.3, which="both")
160
+ for ax in axes:
161
+ ax.legend(fontsize=6, ncol=1)
162
+ fig.tight_layout()
163
+ fig.savefig(Path(args.out, "curves.png"), dpi=130)
164
+ plt.close(fig)
165
+
166
+ summary = dict(n_runs=len(rows), runs=rows, tb_coverage=cov)
167
+ with open(Path(args.out, "summary.json"), "w") as f:
168
+ json.dump(summary, f, indent=2)
169
+ print(json.dumps(dict(n_runs=len(rows),
170
+ runs=[(x["exp"], x["final_t_can"], x["best_t_can"])
171
+ for x in rows]), indent=2))
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()