dsh-eval-guard 0.7.2 → 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.
- package/ISSUES.md +239 -0
- package/README.md +48 -18
- package/assets/neural-cifar/workspace.zip +0 -0
- package/assets/neural-cpp/workspace.zip +0 -0
- package/assets/neural-fp32/workspace.zip +0 -0
- package/assets/neural-pendulum/SPEC.md +43 -35
- package/assets/neural-pendulum/TASK.md +139 -33
- package/assets/neural-pendulum/rl-feasibility/README.md +60 -0
- package/assets/neural-pendulum/rl-feasibility/RESULTS.md +370 -0
- package/assets/neural-pendulum/rl-feasibility/evaluate.py +107 -0
- package/assets/neural-pendulum/rl-feasibility/finalize_run.py +168 -0
- package/assets/neural-pendulum/rl-feasibility/models.py +124 -0
- package/assets/neural-pendulum/rl-feasibility/obs.py +205 -0
- package/assets/neural-pendulum/rl-feasibility/plot_selected.py +55 -0
- package/assets/neural-pendulum/rl-feasibility/reference_controller.py +112 -0
- package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
- package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +417 -0
- package/assets/neural-pendulum/rl-feasibility/results/summary.json +1599 -0
- package/assets/neural-pendulum/rl-feasibility/results/table.md +39 -0
- package/assets/neural-pendulum/rl-feasibility/sim.py +324 -0
- package/assets/neural-pendulum/rl-feasibility/summarize.py +175 -0
- package/assets/neural-pendulum/rl-feasibility/task.py +342 -0
- package/assets/neural-pendulum/rl-feasibility/test_contract.py +188 -0
- package/assets/neural-pendulum/rl-feasibility/train_ppo.py +403 -0
- package/assets/neural-pendulum/rl-feasibility/train_sac.py +406 -0
- package/assets/neural-pendulum/sim.cpp +13 -2
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/README.md +19 -9
- package/eval/bomberman.py +5 -3
- package/eval/commit0.py +10 -3
- package/eval/common.py +169 -5
- package/eval/ctf.py +4 -2
- package/eval/featurebench.py +12 -8
- package/eval/land.py +4 -2
- package/eval/legacy/bomberman.py +5 -2
- package/eval/legacy/ctf.py +5 -2
- package/eval/legacy/featurebench.py +55 -15
- package/eval/legacy/land.py +6 -3
- package/eval/legacy/swe_bench_pro.py +57 -16
- package/eval/legacy/tank.py +2 -2
- package/eval/legacy/warehouse.py +4 -4
- package/eval/neural-cpp.py +9 -6
- package/eval/neural-fp32.py +17 -5
- package/eval/swe-bench-pro.py +11 -7
- package/eval/tank.py +11 -2
- package/eval/warehouse.py +17 -1
- package/lib/client/index.js +56 -29
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +883 -298
- package/native/dsh-eval-supervisor +0 -0
- package/native/supervisor.c +232 -36
- package/neural-cifar-eval/README.md +5 -0
- package/neural-cifar-eval/common.py +13 -0
- package/neural-cifar-eval/ops_eval.py +17 -5
- package/neural-cifar-eval/run_eval.py +42 -32
- package/neural-pendulum-eval/README.md +62 -0
- package/neural-pendulum-eval/ops_eval.py +603 -0
- package/neural-pendulum-eval/run_eval.py +218 -137
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +7 -11
- package/assets/neural-pendulum/hidden/test_infra.cpp +0 -631
- package/assets/neural-pendulum/include/nn/nn.hpp +0 -88
|
@@ -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()
|