dsh-eval-guard 0.7.4 → 0.7.5
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 +2 -2
- package/README.md +1 -1
- package/assets/neural-pendulum/SPEC.md +2 -0
- package/assets/neural-pendulum/TASK.md +3 -1
- package/assets/neural-pendulum/sim.cpp +25 -3
- package/assets/neural-pendulum/sim_agent +0 -0
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/neural-pendulum-eval/README.md +4 -2
- package/neural-pendulum-eval/run_eval.py +3 -1
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +1 -1
- package/assets/neural-pendulum/rl-feasibility/README.md +0 -60
- package/assets/neural-pendulum/rl-feasibility/RESULTS.md +0 -370
- package/assets/neural-pendulum/rl-feasibility/evaluate.py +0 -107
- package/assets/neural-pendulum/rl-feasibility/finalize_run.py +0 -168
- package/assets/neural-pendulum/rl-feasibility/models.py +0 -124
- package/assets/neural-pendulum/rl-feasibility/obs.py +0 -205
- package/assets/neural-pendulum/rl-feasibility/plot_selected.py +0 -55
- package/assets/neural-pendulum/rl-feasibility/reference_controller.py +0 -112
- package/assets/neural-pendulum/rl-feasibility/results/curves_selected.png +0 -0
- package/assets/neural-pendulum/rl-feasibility/results/reference_controller.json +0 -417
- package/assets/neural-pendulum/rl-feasibility/results/summary.json +0 -1599
- package/assets/neural-pendulum/rl-feasibility/results/table.md +0 -39
- package/assets/neural-pendulum/rl-feasibility/sim.py +0 -324
- package/assets/neural-pendulum/rl-feasibility/summarize.py +0 -175
- package/assets/neural-pendulum/rl-feasibility/task.py +0 -342
- package/assets/neural-pendulum/rl-feasibility/test_contract.py +0 -188
- package/assets/neural-pendulum/rl-feasibility/train_ppo.py +0 -403
- package/assets/neural-pendulum/rl-feasibility/train_sac.py +0 -406
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
"""Trainer-side task wrapper: observation, reward, episode bookkeeping.
|
|
2
|
-
|
|
3
|
-
Nothing in this file is part of the simulator API. The simulator returns only
|
|
4
|
-
the three angles; this module turns that angle stream into
|
|
5
|
-
|
|
6
|
-
* the policy observation (obs.py),
|
|
7
|
-
* the training reward,
|
|
8
|
-
* success / t_can / ever_up / episode-return bookkeeping,
|
|
9
|
-
* reset initial-state distributions.
|
|
10
|
-
|
|
11
|
-
Reward (entirely constructed here, never in the environment):
|
|
12
|
-
a = wrap_to_pi(theta - pi)
|
|
13
|
-
s2 = sum_i a_i^2
|
|
14
|
-
upf = 1[max_i |a_i| < 0.15]
|
|
15
|
-
tip_y = -sum_i l_i cos(theta_i) (function of theta)
|
|
16
|
-
E ~ 0.5 da^T M(theta) da - g sum_i gvec_i cos(theta_i)
|
|
17
|
-
(da is the manual finite-difference velocity from obs.py)
|
|
18
|
-
r = exp(-2 s2) + 2 exp(-50 s2) - 0.05 s2
|
|
19
|
-
- 0.001 sum da^2 - 0.01 sum u^2 + 3 upf
|
|
20
|
-
+ 0.3 (tip_y + L)/(2L) - 0.01 |E - E_up|/(E_up - E_down)
|
|
21
|
-
|
|
22
|
-
The last two shaping terms are functions of the angle stream (and da), not
|
|
23
|
-
privileged simulator outputs. Because the simulator exposes no omega, da is
|
|
24
|
-
used wherever the reference reward used omega; this is documented in
|
|
25
|
-
RESULTS.md.
|
|
26
|
-
|
|
27
|
-
Reset distributions:
|
|
28
|
-
training random : theta_i ~ U(-0.05, 0.05), omega_i ~ U(-0.02, 0.02)
|
|
29
|
-
(no fixed bias of any kind)
|
|
30
|
-
training fixed : theta = [0.013, -0.008, 0.021], omega = 0
|
|
31
|
-
evaluation : theta = [0.013, -0.008, 0.021], omega = 0 (no noise)
|
|
32
|
-
"""
|
|
33
|
-
from __future__ import annotations
|
|
34
|
-
|
|
35
|
-
import numpy as np
|
|
36
|
-
import torch
|
|
37
|
-
|
|
38
|
-
from obs import (DT, H, NumpyAngleObs, OBS_MODES, TorchAngleObs,
|
|
39
|
-
make_numpy_obs, make_torch_obs, obs_spec, wrap_to_pi)
|
|
40
|
-
from sim import (BONUS_TOL, N_ACT, N_LINKS, Params, UP_TOL, energy_down,
|
|
41
|
-
energy_up, make_torch_consts, tip_pos, torch_tip, total_energy)
|
|
42
|
-
|
|
43
|
-
TRAIN_THETA_HALF = 0.05
|
|
44
|
-
TRAIN_OMEGA_HALF = 0.02
|
|
45
|
-
EVAL_THETA0 = np.array([0.013, -0.008, 0.021], dtype=np.float64)
|
|
46
|
-
EVAL_OMEGA0 = np.zeros(N_LINKS, dtype=np.float64)
|
|
47
|
-
|
|
48
|
-
DEFAULT_W = dict(exp=1.0, sharp=2.0, ang=0.05, vel=0.001, act=0.01, up=3.0,
|
|
49
|
-
tip=0.3, energy=0.01)
|
|
50
|
-
|
|
51
|
-
# Reward presets. Every term is a trainer-side function of theta, the manual
|
|
52
|
-
# difference da and the action; the keys map 1:1 onto the reward formula in
|
|
53
|
-
# reward_np / TorchReward. No simulator omega is read.
|
|
54
|
-
#
|
|
55
|
-
# base : the original dense shaping above.
|
|
56
|
-
# sparse : up-focused, almost no dense shaping:
|
|
57
|
-
# r = 0.2 exp(-2 s2) + 3.0 1[max|a| < 0.15]
|
|
58
|
-
# - 0.001 sum da^2 - 0.01 sum u^2
|
|
59
|
-
# (no angle-progress, tip or energy terms)
|
|
60
|
-
# energy : strong energy shaping toward the upright energy, weaker angle and
|
|
61
|
-
# velocity penalties:
|
|
62
|
-
# r = 0.2 exp(-2 s2) + 0.5 exp(-50 s2) - 0.01 s2
|
|
63
|
-
# - 0.0002 sum da^2 - 0.005 sum u^2
|
|
64
|
-
# + 3.0 1[max|a| < 0.15]
|
|
65
|
-
# - 0.5 |E - E_up| / (E_up - E_down)
|
|
66
|
-
# E is the trainer-side mechanical energy using da as velocity.
|
|
67
|
-
SPARSE_W = dict(exp=0.2, sharp=0.0, ang=0.0, vel=0.001, act=0.01, up=3.0,
|
|
68
|
-
tip=0.0, energy=0.0)
|
|
69
|
-
ENERGY_W = dict(exp=1.0, sharp=2.0, ang=0.05, vel=0.0, act=0.005, up=3.0,
|
|
70
|
-
tip=0.0, energy=0.5)
|
|
71
|
-
|
|
72
|
-
REWARD_PRESETS = {
|
|
73
|
-
"base": dict(DEFAULT_W),
|
|
74
|
-
"sparse": dict(SPARSE_W),
|
|
75
|
-
"energy": dict(ENERGY_W),
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# The energy error is |E-E_up|/span. At high spin rates E grows without bound,
|
|
80
|
-
# and with weight 0.5 (energy preset) the raw term produced huge negative
|
|
81
|
-
# rewards and value-loss blow-up (~5e7). Clamp it: the gradient near upright
|
|
82
|
-
# is unchanged, but violent spins cannot dominate the return.
|
|
83
|
-
E_ERR_CLAMP = 3.0
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def get_reward_preset(name: str) -> dict:
|
|
87
|
-
if name not in REWARD_PRESETS:
|
|
88
|
-
raise ValueError(f"unknown reward preset {name!r}; "
|
|
89
|
-
f"choose from {sorted(REWARD_PRESETS)}")
|
|
90
|
-
return dict(REWARD_PRESETS[name])
|
|
91
|
-
|
|
92
|
-
OBS_SPEC = obs_spec("full")
|
|
93
|
-
|
|
94
|
-
TORCH_REWARD_NOTE = (
|
|
95
|
-
"Reward is built in task.py from theta and the manual difference da only. "
|
|
96
|
-
"The simulator returns angles only (no reward/done/omega/tip). tip_y and "
|
|
97
|
-
"the energy-like term are trainer-side functions of theta (and da), not "
|
|
98
|
-
"simulator outputs.")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def reward_np(theta, da, u, p: Params, w=None):
|
|
102
|
-
w = DEFAULT_W if w is None else w
|
|
103
|
-
theta = np.asarray(theta, dtype=np.float64).reshape(N_LINKS)
|
|
104
|
-
da = np.asarray(da, dtype=np.float64).reshape(N_LINKS)
|
|
105
|
-
u = np.asarray(u, dtype=np.float64).reshape(N_ACT)
|
|
106
|
-
a = wrap_to_pi(theta - np.pi)
|
|
107
|
-
s2 = float(a @ a)
|
|
108
|
-
upf = float(np.abs(a).max() < BONUS_TOL)
|
|
109
|
-
y = tip_pos(theta, p)[1]
|
|
110
|
-
L = p.L_total
|
|
111
|
-
e = total_energy(theta, da, p)[0]
|
|
112
|
-
e_err = min(E_ERR_CLAMP,
|
|
113
|
-
abs(e - energy_up(p)) / max(1e-6, energy_up(p) - energy_down(p)))
|
|
114
|
-
return (w["exp"] * np.exp(-2.0 * s2)
|
|
115
|
-
+ w["sharp"] * np.exp(-50.0 * s2)
|
|
116
|
-
- w["ang"] * s2
|
|
117
|
-
- w["vel"] * float(da @ da)
|
|
118
|
-
- w["act"] * float(u @ u)
|
|
119
|
-
+ w["up"] * upf
|
|
120
|
-
+ w["tip"] * (y + L) / (2.0 * L)
|
|
121
|
-
- w["energy"] * e_err)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
class TorchReward:
|
|
125
|
-
def __init__(self, p: Params, w=None, device="cpu"):
|
|
126
|
-
self.p = p
|
|
127
|
-
self.w = dict(DEFAULT_W if w is None else w)
|
|
128
|
-
self.c = make_torch_consts(p, device)
|
|
129
|
-
self.e_up = energy_up(p)
|
|
130
|
-
self.e_span = max(1e-6, energy_up(p) - energy_down(p))
|
|
131
|
-
self.L = p.L_total
|
|
132
|
-
|
|
133
|
-
def __call__(self, theta, da, u):
|
|
134
|
-
t = torch
|
|
135
|
-
a = t.remainder(theta - np.pi + np.pi, 2 * np.pi) - np.pi
|
|
136
|
-
s2 = (a * a).sum(1)
|
|
137
|
-
upf = (a.abs().amax(1) < BONUS_TOL).float()
|
|
138
|
-
y = torch_tip(theta, self.c)[:, 1]
|
|
139
|
-
M = self.c["A"] * t.cos(theta.unsqueeze(2) - theta.unsqueeze(1))
|
|
140
|
-
E = 0.5 * (da * t.linalg.solve(M, da.unsqueeze(-1)).squeeze(-1)).sum(1)
|
|
141
|
-
E = E - self.c["g"] * (t.cos(theta) * self.c["vcoef"]).sum(1)
|
|
142
|
-
e_err = ((E - self.e_up).abs() / self.e_span).clamp(max=E_ERR_CLAMP)
|
|
143
|
-
w = self.w
|
|
144
|
-
return (w["exp"] * t.exp(-2.0 * s2)
|
|
145
|
-
+ w["sharp"] * t.exp(-50.0 * s2)
|
|
146
|
-
- w["ang"] * s2
|
|
147
|
-
- w["vel"] * (da * da).sum(1)
|
|
148
|
-
- w["act"] * (u * u).sum(1)
|
|
149
|
-
+ w["up"] * upf
|
|
150
|
-
+ w["tip"] * (y + self.L) / (2.0 * self.L)
|
|
151
|
-
- w["energy"] * e_err)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
class NumpyTracker:
|
|
155
|
-
def __init__(self, up_tol=UP_TOL):
|
|
156
|
-
self.up_tol = up_tol
|
|
157
|
-
self.t = 0.0
|
|
158
|
-
self.ep_return = 0.0
|
|
159
|
-
self.cur_up_start = -1.0
|
|
160
|
-
self.t_can = 0.0
|
|
161
|
-
self.ever_up = False
|
|
162
|
-
self.first_up_t = -1.0
|
|
163
|
-
self.up_steps = 0
|
|
164
|
-
|
|
165
|
-
def reset(self):
|
|
166
|
-
self.t = 0.0
|
|
167
|
-
self.ep_return = 0.0
|
|
168
|
-
self.cur_up_start = -1.0
|
|
169
|
-
self.t_can = 0.0
|
|
170
|
-
self.ever_up = False
|
|
171
|
-
self.first_up_t = -1.0
|
|
172
|
-
self.up_steps = 0
|
|
173
|
-
|
|
174
|
-
def update(self, theta, r):
|
|
175
|
-
a = wrap_to_pi(np.asarray(theta, dtype=np.float64).reshape(N_LINKS) - np.pi)
|
|
176
|
-
up = bool(np.abs(a).max() < self.up_tol)
|
|
177
|
-
self.t += DT
|
|
178
|
-
self.ep_return += float(r)
|
|
179
|
-
if up and not self.ever_up:
|
|
180
|
-
self.ever_up = True
|
|
181
|
-
self.first_up_t = self.t
|
|
182
|
-
if up:
|
|
183
|
-
self.up_steps += 1
|
|
184
|
-
if self.cur_up_start < 0:
|
|
185
|
-
self.cur_up_start = self.t
|
|
186
|
-
self.t_can = max(self.t_can, self.t - self.cur_up_start)
|
|
187
|
-
else:
|
|
188
|
-
self.cur_up_start = -1.0
|
|
189
|
-
|
|
190
|
-
def summary(self):
|
|
191
|
-
return dict(ep_return=float(self.ep_return), t_can=float(self.t_can),
|
|
192
|
-
ever_up=float(self.ever_up),
|
|
193
|
-
first_up_t=float(self.first_up_t),
|
|
194
|
-
up_frac=float(self.up_steps * DT / max(self.t, 1e-6)),
|
|
195
|
-
length=int(round(self.t / DT)))
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
class TorchTracker:
|
|
199
|
-
def __init__(self, n, up_tol=UP_TOL, device="cpu"):
|
|
200
|
-
self.torch = torch
|
|
201
|
-
self.n = int(n)
|
|
202
|
-
self.up_tol = up_tol
|
|
203
|
-
self.device = device
|
|
204
|
-
self.t = torch.zeros(n, device=device)
|
|
205
|
-
self.ep_return = torch.zeros(n, device=device)
|
|
206
|
-
self.cur_up_start = torch.full((n,), -1.0, device=device)
|
|
207
|
-
self.t_can = torch.zeros(n, device=device)
|
|
208
|
-
self.ever_up = torch.zeros(n, dtype=torch.bool, device=device)
|
|
209
|
-
self.first_up_t = torch.full((n,), -1.0, device=device)
|
|
210
|
-
self.up_steps = torch.zeros(n, device=device)
|
|
211
|
-
|
|
212
|
-
def reset(self, idx=None):
|
|
213
|
-
t = self.torch
|
|
214
|
-
if idx is None:
|
|
215
|
-
idx = t.arange(self.n, device=self.device)
|
|
216
|
-
idx = t.as_tensor(idx, dtype=t.long, device=self.device).flatten()
|
|
217
|
-
self.t[idx] = 0.0
|
|
218
|
-
self.ep_return[idx] = 0.0
|
|
219
|
-
self.cur_up_start[idx] = -1.0
|
|
220
|
-
self.t_can[idx] = 0.0
|
|
221
|
-
self.ever_up[idx] = False
|
|
222
|
-
self.first_up_t[idx] = -1.0
|
|
223
|
-
self.up_steps[idx] = 0.0
|
|
224
|
-
|
|
225
|
-
def update(self, theta, r):
|
|
226
|
-
t = self.torch
|
|
227
|
-
a = t.remainder(theta - np.pi + np.pi, 2 * np.pi) - np.pi
|
|
228
|
-
up = a.abs().amax(1) < self.up_tol
|
|
229
|
-
self.t = self.t + DT
|
|
230
|
-
self.ep_return = self.ep_return + r
|
|
231
|
-
newly = up & (~self.ever_up)
|
|
232
|
-
self.ever_up = self.ever_up | up
|
|
233
|
-
self.first_up_t = t.where(newly, self.t, self.first_up_t)
|
|
234
|
-
self.up_steps = self.up_steps + up.float()
|
|
235
|
-
start = t.where(up & (self.cur_up_start < 0), self.t, self.cur_up_start)
|
|
236
|
-
self.cur_up_start = t.where(up, start, t.full_like(start, -1.0))
|
|
237
|
-
streak = t.where(self.cur_up_start >= 0, self.t - self.cur_up_start,
|
|
238
|
-
t.zeros_like(self.t))
|
|
239
|
-
self.t_can = t.maximum(self.t_can, streak)
|
|
240
|
-
|
|
241
|
-
def summary(self, idx):
|
|
242
|
-
t = self.torch
|
|
243
|
-
idx = t.as_tensor(idx, dtype=t.long, device=self.device).flatten()
|
|
244
|
-
return dict(ep_return=self.ep_return[idx].clone(),
|
|
245
|
-
t_can=self.t_can[idx].clone(),
|
|
246
|
-
ever_up=self.ever_up[idx].float().clone(),
|
|
247
|
-
first_up_t=self.first_up_t[idx].clone(),
|
|
248
|
-
up_frac=self.up_steps[idx] * DT / self.t[idx].clamp(min=1e-6),
|
|
249
|
-
length=(self.t[idx] / DT).round().long())
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def sample_train_init(n, gen=None, device="cpu"):
|
|
253
|
-
"""Pure uniform noise, no fixed bias: theta U(-.05,.05), omega U(-.02,.02)."""
|
|
254
|
-
t = torch
|
|
255
|
-
theta = (t.rand(n, N_LINKS, generator=gen, device=device) * 2.0 - 1.0) * TRAIN_THETA_HALF
|
|
256
|
-
omega = (t.rand(n, N_LINKS, generator=gen, device=device) * 2.0 - 1.0) * TRAIN_OMEGA_HALF
|
|
257
|
-
return theta, omega
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def sample_upright_init(n, gen=None, device="cpu"):
|
|
261
|
-
"""Curriculum-only: small perturbation around upright (theta = pi)."""
|
|
262
|
-
t = torch
|
|
263
|
-
theta = np.pi + (t.rand(n, N_LINKS, generator=gen, device=device) * 2.0 - 1.0) * TRAIN_THETA_HALF
|
|
264
|
-
omega = (t.rand(n, N_LINKS, generator=gen, device=device) * 2.0 - 1.0) * TRAIN_OMEGA_HALF
|
|
265
|
-
return theta, omega
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def fixed_init(n, device="cpu"):
|
|
269
|
-
theta = torch.tensor(EVAL_THETA0, dtype=torch.float32, device=device).repeat(n, 1)
|
|
270
|
-
omega = torch.zeros(n, N_LINKS, dtype=torch.float32, device=device)
|
|
271
|
-
return theta, omega
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def make_init(n, mode="rand", gen=None, device="cpu", up_frac=0.5):
|
|
275
|
-
if mode == "rand":
|
|
276
|
-
return sample_train_init(n, gen=gen, device=device)
|
|
277
|
-
if mode == "fixed":
|
|
278
|
-
return fixed_init(n, device=device)
|
|
279
|
-
if mode == "mix":
|
|
280
|
-
# Curriculum: a fraction up_frac of resets start near upright so the
|
|
281
|
-
# balance phase is actually visited; the rest start near down.
|
|
282
|
-
n_up = int(round(float(up_frac) * n))
|
|
283
|
-
n_down = n - n_up
|
|
284
|
-
th_d, om_d = sample_train_init(n_down, gen=gen, device=device)
|
|
285
|
-
th_u, om_u = sample_upright_init(n_up, gen=gen, device=device)
|
|
286
|
-
theta = torch.cat([th_d, th_u], dim=0)
|
|
287
|
-
omega = torch.cat([om_d, om_u], dim=0)
|
|
288
|
-
perm = torch.randperm(n, generator=gen, device=device)
|
|
289
|
-
return theta[perm], omega[perm]
|
|
290
|
-
raise ValueError(mode)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
class NumpyTask:
|
|
294
|
-
def __init__(self, p: Params, reward_w=None, obs_mode="full"):
|
|
295
|
-
self.p = p
|
|
296
|
-
self.w = dict(DEFAULT_W if reward_w is None else reward_w)
|
|
297
|
-
self.obs_mode = obs_mode
|
|
298
|
-
self.builder = make_numpy_obs(obs_mode)
|
|
299
|
-
self.tracker = NumpyTracker()
|
|
300
|
-
|
|
301
|
-
def reset(self, theta):
|
|
302
|
-
self.tracker.reset()
|
|
303
|
-
return self.builder.reset(theta)
|
|
304
|
-
|
|
305
|
-
def step(self, theta, u):
|
|
306
|
-
obs = self.builder.update(theta)
|
|
307
|
-
r = reward_np(theta, self.builder.da, u, self.p, self.w)
|
|
308
|
-
self.tracker.update(theta, r)
|
|
309
|
-
return obs, r
|
|
310
|
-
|
|
311
|
-
@property
|
|
312
|
-
def obs(self):
|
|
313
|
-
return self.builder.obs
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
class TorchTask:
|
|
317
|
-
def __init__(self, p: Params, n: int, reward_w=None, device="cpu",
|
|
318
|
-
obs_mode="full"):
|
|
319
|
-
self.p = p
|
|
320
|
-
self.n = int(n)
|
|
321
|
-
self.w = dict(DEFAULT_W if reward_w is None else reward_w)
|
|
322
|
-
self.obs_mode = obs_mode
|
|
323
|
-
self.builder = make_torch_obs(n, obs_mode, device=device)
|
|
324
|
-
self.reward = TorchReward(p, self.w, device)
|
|
325
|
-
self.tracker = TorchTracker(n, device=device)
|
|
326
|
-
|
|
327
|
-
def reset(self, theta, idx=None):
|
|
328
|
-
self.tracker.reset(idx)
|
|
329
|
-
return self.builder.reset(theta, idx)
|
|
330
|
-
|
|
331
|
-
def step(self, theta, u):
|
|
332
|
-
obs = self.builder.update(theta)
|
|
333
|
-
r = self.reward(theta, self.builder.da, u)
|
|
334
|
-
self.tracker.update(theta, r)
|
|
335
|
-
return obs, r
|
|
336
|
-
|
|
337
|
-
@property
|
|
338
|
-
def obs(self):
|
|
339
|
-
return self.builder.obs()
|
|
340
|
-
|
|
341
|
-
def summary(self, idx):
|
|
342
|
-
return self.tracker.summary(idx)
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"""Contract checks for the v2 random-init / SAC feasibility round.
|
|
2
|
-
|
|
3
|
-
Checks, in order:
|
|
4
|
-
1. simulator API returns three angles only -- no reward/loss/done/info;
|
|
5
|
-
2. the 3000-step budget raises EpisodeLengthError and reset() restores it
|
|
6
|
-
(single and batched simulators, including partial resets);
|
|
7
|
-
3. training reset is pure noise U(-.05,.05) / U(-.02,.02) with no bias;
|
|
8
|
-
the fixed control and the evaluation use [0.013,-0.008,0.021], omega=0;
|
|
9
|
-
4. observation definition: a=wrap(theta-pi), da=wrap(a-a_prev)/dt with
|
|
10
|
-
da(0)=0, frame=[cos a, sin a, da/5], H=4, 36 dims, theta not /pi;
|
|
11
|
-
5. the observation builder cannot see omega (signature check) and the torch
|
|
12
|
-
builder matches numpy;
|
|
13
|
-
6. t_can/success bookkeeping holds a streak and resets on leaving upright;
|
|
14
|
-
7. trainer-side reward is finite and the fixed-init evaluation runs.
|
|
15
|
-
"""
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import inspect
|
|
19
|
-
|
|
20
|
-
import numpy as np
|
|
21
|
-
import torch
|
|
22
|
-
|
|
23
|
-
from evaluate import evaluate
|
|
24
|
-
from obs import DT, H, NumpyAngleObs, OBS_DIM, TorchAngleObs, wrap_to_pi
|
|
25
|
-
from sim import (EPISODE_STEPS, EpisodeLengthError, Params, PendulumSim,
|
|
26
|
-
TorchVecSim)
|
|
27
|
-
from task import (EVAL_THETA0, NumpyTask, TorchTracker, make_init,
|
|
28
|
-
sample_train_init)
|
|
29
|
-
|
|
30
|
-
p = Params()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def frame(a, da):
|
|
34
|
-
cs = np.empty(6)
|
|
35
|
-
cs[0::2] = np.cos(a)
|
|
36
|
-
cs[1::2] = np.sin(a)
|
|
37
|
-
return np.concatenate([cs, np.asarray(da) / 5.0])
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
print("obs_dim", OBS_DIM)
|
|
41
|
-
assert OBS_DIM == 36 and H == 4
|
|
42
|
-
|
|
43
|
-
# --- 1. angle-only API ------------------------------------------------------
|
|
44
|
-
sim = PendulumSim(p)
|
|
45
|
-
th0 = np.array([0.013, -0.008, 0.021])
|
|
46
|
-
out = sim.reset(th0, np.zeros(3))
|
|
47
|
-
assert isinstance(out, np.ndarray) and out.shape == (3,)
|
|
48
|
-
assert np.allclose(out, th0)
|
|
49
|
-
out = sim.step(np.array([0.1, -0.2]))
|
|
50
|
-
assert isinstance(out, np.ndarray) and out.shape == (3,)
|
|
51
|
-
assert len(inspect.signature(PendulumSim.step).parameters) == 2
|
|
52
|
-
assert "reward" not in inspect.getsource(PendulumSim.step).lower()
|
|
53
|
-
assert not hasattr(sim, "omega") and not hasattr(sim, "done")
|
|
54
|
-
print("single sim step returns only 3 angles; no public omega/done")
|
|
55
|
-
|
|
56
|
-
# --- 2. episode budget ------------------------------------------------------
|
|
57
|
-
short = PendulumSim(p, episode_steps=5)
|
|
58
|
-
short.reset(th0, np.zeros(3))
|
|
59
|
-
for _ in range(5):
|
|
60
|
-
short.step(np.zeros(2))
|
|
61
|
-
try:
|
|
62
|
-
short.step(np.zeros(2))
|
|
63
|
-
raise AssertionError("expected EpisodeLengthError")
|
|
64
|
-
except EpisodeLengthError as e:
|
|
65
|
-
print("single sim budget error:", str(e)[:60])
|
|
66
|
-
short.reset(th0, np.zeros(3))
|
|
67
|
-
short.step(np.zeros(2))
|
|
68
|
-
|
|
69
|
-
vec = TorchVecSim(p, n=2, episode_steps=5)
|
|
70
|
-
assert not hasattr(vec, "omega") and not hasattr(vec, "done")
|
|
71
|
-
vec.reset(torch.tensor([th0, th0]), torch.zeros(2, 3))
|
|
72
|
-
for _ in range(5):
|
|
73
|
-
vec.step(torch.zeros(2, 2))
|
|
74
|
-
try:
|
|
75
|
-
vec.step(torch.zeros(2, 2))
|
|
76
|
-
raise AssertionError("expected batched EpisodeLengthError")
|
|
77
|
-
except EpisodeLengthError as e:
|
|
78
|
-
print("vec sim budget error:", str(e)[:70])
|
|
79
|
-
# resetting only one env must still raise because env 1 is exhausted
|
|
80
|
-
vec.reset(torch.tensor([th0]), torch.zeros(1, 3), idx=torch.tensor([0]))
|
|
81
|
-
try:
|
|
82
|
-
vec.step(torch.zeros(2, 2))
|
|
83
|
-
raise AssertionError("expected error while env 1 exhausted")
|
|
84
|
-
except EpisodeLengthError:
|
|
85
|
-
pass
|
|
86
|
-
vec.reset(torch.tensor([th0, th0]), torch.zeros(2, 3))
|
|
87
|
-
vec.step(torch.zeros(2, 2))
|
|
88
|
-
print("batched sim budget + partial reset contract OK")
|
|
89
|
-
|
|
90
|
-
# --- 3. reset distributions -------------------------------------------------
|
|
91
|
-
gen = torch.Generator().manual_seed(0)
|
|
92
|
-
th, om = sample_train_init(20000, gen)
|
|
93
|
-
assert th.abs().max() <= 0.05 + 1e-6 and om.abs().max() <= 0.02 + 1e-6
|
|
94
|
-
assert th.abs().max() > 0.049 and om.abs().max() > 0.019
|
|
95
|
-
assert th.mean().abs() < 0.002 and om.mean().abs() < 0.001, (th.mean(), om.mean())
|
|
96
|
-
print("random init ranges", float(th.min()), float(th.max()),
|
|
97
|
-
float(om.min()), float(om.max()), "means", float(th.mean()), float(om.mean()))
|
|
98
|
-
|
|
99
|
-
thf, omf = make_init(4, "fixed")
|
|
100
|
-
assert np.allclose(thf.numpy(), np.tile(EVAL_THETA0, (4, 1)))
|
|
101
|
-
assert np.allclose(omf.numpy(), 0.0)
|
|
102
|
-
thf2, omf2 = make_init(4, "rand", gen)
|
|
103
|
-
print("fixed init", thf[0].tolist(), "eval", EVAL_THETA0.tolist())
|
|
104
|
-
|
|
105
|
-
# --- 4. observation definition ---------------------------------------------
|
|
106
|
-
th0 = np.array([0.013, -0.008, 0.021])
|
|
107
|
-
th1 = np.array([0.063, -0.048, 0.001])
|
|
108
|
-
b = NumpyAngleObs()
|
|
109
|
-
o0 = b.reset(th0)
|
|
110
|
-
a0 = wrap_to_pi(th0 - np.pi)
|
|
111
|
-
want0 = np.tile(frame(a0, np.zeros(3)), H)
|
|
112
|
-
assert np.allclose(o0, want0, atol=1e-6)
|
|
113
|
-
o1 = b.update(th1)
|
|
114
|
-
a1 = wrap_to_pi(th1 - np.pi)
|
|
115
|
-
da = wrap_to_pi(a1 - a0) / DT
|
|
116
|
-
want_last = frame(a1, da)
|
|
117
|
-
mat = o1.reshape(H, 9)
|
|
118
|
-
assert np.allclose(mat[-1], want_last, atol=1e-6)
|
|
119
|
-
assert np.allclose(mat[:-1], np.tile(want0.reshape(H, 9)[0], (H - 1, 1)), atol=1e-6)
|
|
120
|
-
# theta is not divided by pi: a=theta-pi, not theta/pi-pi
|
|
121
|
-
assert np.allclose(mat[-1, :2], [np.cos(a1[0]), np.sin(a1[0])], atol=1e-6)
|
|
122
|
-
assert not np.allclose(mat[-1, :2], [np.cos(th1[0] / np.pi - np.pi),
|
|
123
|
-
np.sin(th1[0] / np.pi - np.pi)], atol=1e-3)
|
|
124
|
-
# da initialisation is exactly zero
|
|
125
|
-
assert np.allclose(o0.reshape(H, 9)[:, 6:], 0.0)
|
|
126
|
-
print("obs frame(last)", np.round(mat[-1], 4), "da", np.round(da, 4))
|
|
127
|
-
|
|
128
|
-
# wrap in the difference: crossing the -pi/pi boundary must not spike
|
|
129
|
-
b.reset(np.array([np.pi - 0.01, 0.0, 0.0]))
|
|
130
|
-
o = b.update(np.array([-np.pi + 0.01, 0.0, 0.0])).reshape(H, 9)
|
|
131
|
-
assert abs(o[-1, 6] - (0.02 / DT) / 5.0) < 1e-5, o[-1, 6]
|
|
132
|
-
print("da wrap OK")
|
|
133
|
-
|
|
134
|
-
# --- 5. no omega in observation API; torch matches numpy --------------------
|
|
135
|
-
assert list(inspect.signature(NumpyAngleObs.update).parameters) == ["self", "theta"]
|
|
136
|
-
assert list(inspect.signature(NumpyAngleObs.reset).parameters) == ["self", "theta"]
|
|
137
|
-
assert list(inspect.signature(TorchAngleObs.update).parameters) == ["self", "theta"]
|
|
138
|
-
tb = TorchAngleObs(1)
|
|
139
|
-
ot = tb.reset(torch.tensor(th0, dtype=torch.float32).unsqueeze(0))
|
|
140
|
-
assert np.allclose(ot.numpy().reshape(-1), o0, atol=1e-5)
|
|
141
|
-
ot = tb.update(torch.tensor(th1, dtype=torch.float32).unsqueeze(0))
|
|
142
|
-
assert np.allclose(ot.numpy().reshape(H, 9), mat, atol=1e-5)
|
|
143
|
-
print("numpy/torch obs builders match; no omega argument")
|
|
144
|
-
|
|
145
|
-
# --- 6. success / t_can bookkeeping ----------------------------------------
|
|
146
|
-
tr = TorchTracker(1)
|
|
147
|
-
tr.reset()
|
|
148
|
-
up_theta = torch.tensor([[np.pi + 0.01, np.pi - 0.01, np.pi + 0.02]])
|
|
149
|
-
for _ in range(1500):
|
|
150
|
-
tr.update(up_theta, torch.zeros(1))
|
|
151
|
-
s = tr.summary(torch.tensor([0]))
|
|
152
|
-
assert abs(float(s["t_can"]) - 14.99) < 1e-2, float(s["t_can"])
|
|
153
|
-
assert abs(float(s["up_frac"]) - 1.0) < 1e-2, float(s["up_frac"]) # all steps up
|
|
154
|
-
assert float(s["ever_up"]) == 1.0 and abs(float(s["first_up_t"]) - 0.01) < 1e-3
|
|
155
|
-
tr.update(torch.tensor([[0.0, 0.0, 0.0]]), torch.zeros(1)) # leave upright
|
|
156
|
-
for _ in range(500):
|
|
157
|
-
tr.update(up_theta, torch.zeros(1))
|
|
158
|
-
s = tr.summary(torch.tensor([0]))
|
|
159
|
-
assert abs(float(s["t_can"]) - 14.99) < 1e-2, float(s["t_can"])
|
|
160
|
-
print("tracker t_can", float(s["t_can"]), "ever_up", float(s["ever_up"]),
|
|
161
|
-
"first_up_t", float(s["first_up_t"]))
|
|
162
|
-
|
|
163
|
-
# --- 7. trainer reward + fixed-init evaluation ------------------------------
|
|
164
|
-
task = NumpyTask(p)
|
|
165
|
-
theta = np.array([0.013, -0.008, 0.021])
|
|
166
|
-
_ = task.reset(theta)
|
|
167
|
-
_, r = task.step(theta, np.array([0.0, 0.0]))
|
|
168
|
-
assert np.isfinite(r)
|
|
169
|
-
print("initial trainer reward", float(r))
|
|
170
|
-
|
|
171
|
-
ev = evaluate(lambda obs: np.zeros(2), p, episode_steps=200)
|
|
172
|
-
assert ev["steps"] == 200 and np.isfinite(ev["ep_return"])
|
|
173
|
-
assert ev["t_can"] == 0.0 and ev["ever_up"] == 0.0
|
|
174
|
-
print("fixed-init eval (200 steps)", {k: ev[k] for k in
|
|
175
|
-
("t_can", "success", "ever_up", "ep_return", "final_ang_err")})
|
|
176
|
-
|
|
177
|
-
# torch/numpy physics agreement on a short rollout
|
|
178
|
-
s1 = PendulumSim(p)
|
|
179
|
-
t1 = s1.reset(th0, np.zeros(3))
|
|
180
|
-
s2 = TorchVecSim(p, n=1)
|
|
181
|
-
s2.reset(torch.tensor(th0, dtype=torch.float32).unsqueeze(0), torch.zeros(1, 3))
|
|
182
|
-
u = np.array([0.3, -0.4])
|
|
183
|
-
for _ in range(20):
|
|
184
|
-
t1 = s1.step(u)
|
|
185
|
-
t2 = s2.step(torch.tensor(u, dtype=torch.float32).unsqueeze(0)).numpy()[0]
|
|
186
|
-
assert np.allclose(t1, t2, atol=1e-4), (t1, t2)
|
|
187
|
-
print("numpy/torch physics agree after 20 steps:", np.round(t1, 6), np.round(t2, 6))
|
|
188
|
-
print("ALL CONTRACT CHECKS PASSED")
|