env-ssl-wrapper 0.4.7__tar.gz → 0.5.1__tar.gz
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.
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/PKG-INFO +3 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/__init__.py +3 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/action_chunk.py +4 -3
- env_ssl_wrapper-0.5.1/env_ssl_wrapper/evaluate.py +340 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/__init__.py +4 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/action_transform_wrapper.py +3 -2
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/adapters.py +50 -27
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/auto_batched_wrapper.py +0 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/mocks.py +47 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/spaces.py +1 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/standardize_env_wrapper.py +1 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/tensor_wrapper.py +9 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/vector.py +1 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/pyproject.toml +3 -1
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/.gitignore +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/LICENSE +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/README.md +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/memory_trace.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/done_tracker_wrapper.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/episode_padding_wrapper.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/helpers.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/image_wrapper.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/standardize_wrapper.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/time_limit_wrapper.py +0 -0
- {env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: env-ssl-wrapper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: One torch-native interface for any MDP environment
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/env-ssl-wrapper/
|
|
6
6
|
Project-URL: Repository, https://codeberg.org/lucidrains/env-ssl-wrapper
|
|
@@ -37,6 +37,8 @@ Requires-Python: >=3.10
|
|
|
37
37
|
Requires-Dist: discrete-continuous-embed-readout
|
|
38
38
|
Requires-Dist: einops>=0.8.1
|
|
39
39
|
Requires-Dist: einx>=0.3.0
|
|
40
|
+
Requires-Dist: imageio
|
|
41
|
+
Requires-Dist: imageio-ffmpeg
|
|
40
42
|
Requires-Dist: memmap-replay-buffer
|
|
41
43
|
Requires-Dist: torch-einops-utils>=0.0.29
|
|
42
44
|
Requires-Dist: torch>=2.5
|
|
@@ -6,6 +6,7 @@ from .standardize import *
|
|
|
6
6
|
|
|
7
7
|
from .memory_trace import MemoryTraceWrapper
|
|
8
8
|
from .action_chunk import ActionChunkWrapper
|
|
9
|
+
from .evaluate import evaluate_actor, EpisodeStats
|
|
9
10
|
|
|
10
11
|
# Wire backwards-compatibility aliases in sys.modules and module globals
|
|
11
12
|
# so imports like `from env_ssl_wrapper.done_tracker_wrapper import DoneTrackerWrapper`
|
|
@@ -40,6 +41,8 @@ __all__ = [
|
|
|
40
41
|
*standardize.__all__,
|
|
41
42
|
'MemoryTraceWrapper',
|
|
42
43
|
'ActionChunkWrapper',
|
|
44
|
+
'evaluate_actor',
|
|
45
|
+
'EpisodeStats',
|
|
43
46
|
]
|
|
44
47
|
|
|
45
48
|
def __getattr__(name):
|
|
@@ -12,6 +12,7 @@ from .standardize.helpers import (
|
|
|
12
12
|
any_true,
|
|
13
13
|
dones_of,
|
|
14
14
|
first_existing,
|
|
15
|
+
get_adapter,
|
|
15
16
|
get_attr,
|
|
16
17
|
is_vectorized,
|
|
17
18
|
to_numpy,
|
|
@@ -70,7 +71,7 @@ class ActionChunkWrapper(EnvWrapper):
|
|
|
70
71
|
self.action_shape = tuple(get_attr(action_space, 'shape', ()) or ())
|
|
71
72
|
|
|
72
73
|
self.expects_batch = is_vectorized(env)
|
|
73
|
-
self.
|
|
74
|
+
self.convert_in = not isinstance(env, EnvWrapper) and not get_adapter(env).torch_native
|
|
74
75
|
|
|
75
76
|
@property
|
|
76
77
|
def chunk_action_shape(self):
|
|
@@ -86,9 +87,9 @@ class ActionChunkWrapper(EnvWrapper):
|
|
|
86
87
|
if not (is_tensor(actions) or isinstance(actions, np.ndarray)):
|
|
87
88
|
actions = np.asarray(actions)
|
|
88
89
|
|
|
89
|
-
# raw simulators only understand numpy
|
|
90
|
+
# raw simulators only understand numpy — wrapper chains and torch-native sims take torch
|
|
90
91
|
|
|
91
|
-
if
|
|
92
|
+
if is_tensor(actions) and self.convert_in:
|
|
92
93
|
actions = to_numpy(actions)
|
|
93
94
|
|
|
94
95
|
chunk_axis = actions.ndim - 1 - len(self.action_shape)
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
from torch import nn, is_tensor
|
|
9
|
+
from torch.distributions import Categorical, Distribution
|
|
10
|
+
from torch.utils._pytree import tree_flatten, tree_map
|
|
11
|
+
|
|
12
|
+
from .standardize.helpers import (
|
|
13
|
+
EnvWrapper,
|
|
14
|
+
env_autoresets,
|
|
15
|
+
env_num_envs,
|
|
16
|
+
env_render,
|
|
17
|
+
exists,
|
|
18
|
+
first_existing,
|
|
19
|
+
get_adapter,
|
|
20
|
+
get_attr,
|
|
21
|
+
is_array,
|
|
22
|
+
is_vectorized,
|
|
23
|
+
normalize_reset_out,
|
|
24
|
+
normalize_step_out,
|
|
25
|
+
to_numpy,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# results
|
|
29
|
+
|
|
30
|
+
class EpisodeStats(NamedTuple):
|
|
31
|
+
returns: torch.Tensor
|
|
32
|
+
lengths: torch.Tensor
|
|
33
|
+
videos: tuple[str, ...] = ()
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def num_episodes(self):
|
|
37
|
+
return self.returns.numel()
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def mean(self):
|
|
41
|
+
return self.returns.mean().item()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def std(self):
|
|
45
|
+
return self.returns.std(unbiased = False).item()
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def min(self):
|
|
49
|
+
return self.returns.min().item()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def max(self):
|
|
53
|
+
return self.returns.max().item()
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def video_path(self):
|
|
57
|
+
return self.videos[0] if self.videos else None
|
|
58
|
+
|
|
59
|
+
def success_rate(self, threshold):
|
|
60
|
+
return (self.returns >= threshold).float().mean().item()
|
|
61
|
+
|
|
62
|
+
def summary(self):
|
|
63
|
+
return dict(episodes = self.num_episodes, mean = self.mean, std = self.std, min = self.min, max = self.max)
|
|
64
|
+
|
|
65
|
+
# actor output -> action
|
|
66
|
+
|
|
67
|
+
def resolve_actor(actor):
|
|
68
|
+
# a callable `.dist` takes precedence, sb3 style
|
|
69
|
+
|
|
70
|
+
module = actor if isinstance(actor, nn.Module) else None
|
|
71
|
+
policy = get_attr(actor, 'dist')
|
|
72
|
+
|
|
73
|
+
if not callable(policy):
|
|
74
|
+
policy = actor
|
|
75
|
+
|
|
76
|
+
assert callable(policy), 'actor must be callable, or expose a callable `.dist`'
|
|
77
|
+
|
|
78
|
+
return policy, module
|
|
79
|
+
|
|
80
|
+
def is_distribution(out):
|
|
81
|
+
if isinstance(out, Distribution):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
if is_tensor(out):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
return callable(get_attr(out, 'sample')) and (exists(get_attr(out, 'mean')) or exists(get_attr(out, 'probs')))
|
|
88
|
+
|
|
89
|
+
def to_action(env, out, deterministic = True):
|
|
90
|
+
# distributions collapse to their mode, raw floating logits over a discrete space to their argmax
|
|
91
|
+
|
|
92
|
+
if is_distribution(out):
|
|
93
|
+
if not deterministic:
|
|
94
|
+
return out.sample()
|
|
95
|
+
|
|
96
|
+
return out.probs.argmax(dim = -1) if exists(get_attr(out, 'probs')) else out.mean
|
|
97
|
+
|
|
98
|
+
if is_tensor(out) and out.is_floating_point() and out.ndim > 0:
|
|
99
|
+
num_actions = get_attr(get_adapter(env).action_space, 'n')
|
|
100
|
+
|
|
101
|
+
if exists(num_actions) and out.shape[-1] == num_actions:
|
|
102
|
+
return out.argmax(dim = -1) if deterministic else Categorical(logits = out).sample()
|
|
103
|
+
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
# states and actions
|
|
107
|
+
|
|
108
|
+
def to_actor_obs(obs, device, batched):
|
|
109
|
+
def convert(x):
|
|
110
|
+
if not (is_array(x) or callable(get_attr(x, '__array__'))):
|
|
111
|
+
return x
|
|
112
|
+
|
|
113
|
+
tensor = x if is_tensor(x) else torch.as_tensor(np.asarray(x))
|
|
114
|
+
|
|
115
|
+
if tensor.is_floating_point():
|
|
116
|
+
tensor = tensor.float()
|
|
117
|
+
|
|
118
|
+
tensor = tensor.to(device)
|
|
119
|
+
|
|
120
|
+
return tensor if batched or tensor.ndim == 0 else tensor[None]
|
|
121
|
+
|
|
122
|
+
return tree_map(convert, obs)
|
|
123
|
+
|
|
124
|
+
def to_env_action(env, action):
|
|
125
|
+
if not is_tensor(action):
|
|
126
|
+
action = torch.as_tensor(action)
|
|
127
|
+
|
|
128
|
+
device = get_attr(env, 'device')
|
|
129
|
+
action = action.to(device) if exists(device) else action
|
|
130
|
+
|
|
131
|
+
# raw simulators take unbatched numpy — torch-native sims take torch as-is (zero-copy)
|
|
132
|
+
|
|
133
|
+
if not isinstance(env, EnvWrapper):
|
|
134
|
+
if get_adapter(env).torch_native:
|
|
135
|
+
return action
|
|
136
|
+
|
|
137
|
+
action = to_numpy(action)
|
|
138
|
+
|
|
139
|
+
if action.ndim > 1 and action.shape[0] == 1:
|
|
140
|
+
action = action[0]
|
|
141
|
+
|
|
142
|
+
return action.item() if action.ndim == 0 else action
|
|
143
|
+
|
|
144
|
+
# wrappers take batched torch actions - pad up to the declared rank
|
|
145
|
+
|
|
146
|
+
chunk_shape = get_attr(env, 'chunk_action_shape')
|
|
147
|
+
|
|
148
|
+
if exists(chunk_shape):
|
|
149
|
+
rank = len(chunk_shape) + 1
|
|
150
|
+
else:
|
|
151
|
+
space = first_existing(env, 'single_action_space', 'action_space')
|
|
152
|
+
rank = len(get_attr(space, 'shape', ())) + 1
|
|
153
|
+
|
|
154
|
+
while action.ndim < rank:
|
|
155
|
+
action = action[None]
|
|
156
|
+
|
|
157
|
+
return action
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# video
|
|
161
|
+
|
|
162
|
+
def capture_frame(env):
|
|
163
|
+
render = get_attr(env, 'render')
|
|
164
|
+
frame = render() if callable(render) else None
|
|
165
|
+
|
|
166
|
+
if not exists(frame):
|
|
167
|
+
try:
|
|
168
|
+
frame = env_render(env, 256, 256)
|
|
169
|
+
except Exception:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
if is_tensor(frame):
|
|
173
|
+
frame = frame.detach().cpu().numpy()
|
|
174
|
+
|
|
175
|
+
frame = np.asarray(frame)
|
|
176
|
+
|
|
177
|
+
if frame.ndim == 4:
|
|
178
|
+
frame = frame[0]
|
|
179
|
+
|
|
180
|
+
if frame.ndim == 3 and frame.shape[0] in (3, 4) and frame.shape[-1] not in (3, 4):
|
|
181
|
+
frame = np.moveaxis(frame, 0, -1)
|
|
182
|
+
|
|
183
|
+
if frame.ndim == 3 and frame.shape[-1] == 4:
|
|
184
|
+
frame = frame[..., :3]
|
|
185
|
+
|
|
186
|
+
if frame.dtype != np.uint8:
|
|
187
|
+
scale = 255. if frame.max() <= 1. else 1.
|
|
188
|
+
frame = np.clip(frame * scale, 0., 255.).astype(np.uint8)
|
|
189
|
+
|
|
190
|
+
return frame
|
|
191
|
+
|
|
192
|
+
def save_video(frames, path, fps = 30):
|
|
193
|
+
import imageio
|
|
194
|
+
|
|
195
|
+
path = Path(path)
|
|
196
|
+
path.parent.mkdir(parents = True, exist_ok = True)
|
|
197
|
+
imageio.mimsave(path, frames, fps = fps, macro_block_size = None)
|
|
198
|
+
|
|
199
|
+
def video_path_at(path, index):
|
|
200
|
+
path = Path(path)
|
|
201
|
+
return str(path if index == 0 else path.with_name(f'{path.stem}_ep{index}{path.suffix}'))
|
|
202
|
+
|
|
203
|
+
# evaluate
|
|
204
|
+
|
|
205
|
+
@torch.no_grad()
|
|
206
|
+
def evaluate_actor(
|
|
207
|
+
actor,
|
|
208
|
+
env,
|
|
209
|
+
episodes = 10,
|
|
210
|
+
max_steps = 1000,
|
|
211
|
+
seed = None,
|
|
212
|
+
goal = None,
|
|
213
|
+
device = None,
|
|
214
|
+
deterministic = True,
|
|
215
|
+
video_path = None,
|
|
216
|
+
video_episodes = 1,
|
|
217
|
+
fps = 30,
|
|
218
|
+
verbose = False
|
|
219
|
+
):
|
|
220
|
+
# seed fixes the episode seeds, so checkpoints are compared on identical episodes
|
|
221
|
+
# video_path records the first video_episodes episodes
|
|
222
|
+
|
|
223
|
+
policy, module = resolve_actor(actor)
|
|
224
|
+
|
|
225
|
+
if not exists(device):
|
|
226
|
+
params = next(module.parameters(), None) if exists(module) else None
|
|
227
|
+
device = get_attr(params, 'device', torch.device('cpu'))
|
|
228
|
+
|
|
229
|
+
was_training = get_attr(module, 'training')
|
|
230
|
+
|
|
231
|
+
if exists(module):
|
|
232
|
+
module.eval()
|
|
233
|
+
|
|
234
|
+
if exists(goal):
|
|
235
|
+
goal = torch.as_tensor(goal) if not is_tensor(goal) else goal
|
|
236
|
+
goal = goal.to(device)
|
|
237
|
+
goal = goal[None] if goal.ndim == 1 else goal
|
|
238
|
+
|
|
239
|
+
num_envs = env_num_envs(env)
|
|
240
|
+
record = exists(video_path)
|
|
241
|
+
|
|
242
|
+
assert not (record and num_envs > 1), 'video recording requires a single env'
|
|
243
|
+
|
|
244
|
+
obs, info = normalize_reset_out(env.reset(seed = seed) if exists(seed) else env.reset())
|
|
245
|
+
batched = is_vectorized(env) or all(map(is_tensor, tree_flatten(obs)[0]))
|
|
246
|
+
|
|
247
|
+
goal = goal.expand(num_envs, -1) if exists(goal) and goal.shape[0] == 1 and num_envs > 1 else goal
|
|
248
|
+
|
|
249
|
+
returns = torch.zeros(num_envs)
|
|
250
|
+
lengths = torch.zeros(num_envs)
|
|
251
|
+
steps = torch.zeros(num_envs, dtype = torch.long)
|
|
252
|
+
|
|
253
|
+
done_returns, done_lengths, videos, frames = [], [], [], []
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
while len(done_returns) < episodes:
|
|
257
|
+
if record and len(done_returns) < video_episodes:
|
|
258
|
+
frame = capture_frame(env)
|
|
259
|
+
if exists(frame):
|
|
260
|
+
frames.append(frame)
|
|
261
|
+
|
|
262
|
+
state = to_actor_obs(obs, device, batched)
|
|
263
|
+
out = policy(state, goal) if exists(goal) else policy(state)
|
|
264
|
+
action = to_env_action(env, to_action(env, out, deterministic))
|
|
265
|
+
|
|
266
|
+
obs, reward, terminated, truncated, info = normalize_step_out(env.step(action))
|
|
267
|
+
|
|
268
|
+
# reward chunks collapse to one scalar per env
|
|
269
|
+
|
|
270
|
+
reward = torch.as_tensor(to_numpy(reward), dtype = torch.float32)
|
|
271
|
+
|
|
272
|
+
while reward.ndim > 1:
|
|
273
|
+
reward = reward.sum(dim = -1)
|
|
274
|
+
|
|
275
|
+
if reward.numel() > num_envs:
|
|
276
|
+
reward = reward.reshape(num_envs, -1).sum(dim = -1)
|
|
277
|
+
|
|
278
|
+
returns += reward if reward.numel() == num_envs else reward.expand(num_envs)
|
|
279
|
+
|
|
280
|
+
lengths += info.get('chunk_length', 1) if isinstance(info, dict) else 1
|
|
281
|
+
steps += 1
|
|
282
|
+
|
|
283
|
+
done = to_numpy(terminated | truncated).astype(bool).reshape(-1)
|
|
284
|
+
|
|
285
|
+
if exists(max_steps):
|
|
286
|
+
done |= to_numpy(steps >= max_steps).reshape(-1)
|
|
287
|
+
|
|
288
|
+
for i in np.where(done)[0]:
|
|
289
|
+
if len(done_returns) >= episodes:
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
episode = len(done_returns)
|
|
293
|
+
done_returns.append(returns[i].item())
|
|
294
|
+
done_lengths.append(lengths[i].item())
|
|
295
|
+
|
|
296
|
+
if verbose:
|
|
297
|
+
print(f'episode {episode:3d} | return {done_returns[-1]:8.1f} | length {done_lengths[-1]:7.0f}', flush = True)
|
|
298
|
+
|
|
299
|
+
if record and episode < video_episodes:
|
|
300
|
+
frame = capture_frame(env)
|
|
301
|
+
if exists(frame):
|
|
302
|
+
frames.append(frame)
|
|
303
|
+
|
|
304
|
+
if frames:
|
|
305
|
+
path = video_path_at(video_path, episode)
|
|
306
|
+
save_video(frames, path, fps)
|
|
307
|
+
videos.append(path)
|
|
308
|
+
|
|
309
|
+
returns[i] = 0.
|
|
310
|
+
lengths[i] = 0.
|
|
311
|
+
steps[i] = 0
|
|
312
|
+
|
|
313
|
+
if done.any() and len(done_returns) < episodes:
|
|
314
|
+
if record:
|
|
315
|
+
frames = []
|
|
316
|
+
|
|
317
|
+
# autoresetting envs reset themselves, everything else resets here
|
|
318
|
+
|
|
319
|
+
if not env_autoresets(env) and (num_envs == 1 or done.all() or bool(get_attr(env, 'needs_reset', False))):
|
|
320
|
+
reset_seed = seed + len(done_returns) if exists(seed) and num_envs == 1 else None
|
|
321
|
+
obs, info = normalize_reset_out(env.reset(seed = reset_seed) if exists(reset_seed) else env.reset())
|
|
322
|
+
finally:
|
|
323
|
+
if exists(module) and exists(was_training):
|
|
324
|
+
module.train(was_training)
|
|
325
|
+
|
|
326
|
+
return EpisodeStats(
|
|
327
|
+
returns = torch.tensor(done_returns, dtype = torch.float32),
|
|
328
|
+
lengths = torch.tensor(done_lengths, dtype = torch.float32),
|
|
329
|
+
videos = tuple(videos)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if __name__ == '__main__':
|
|
333
|
+
import gymnasium as gym
|
|
334
|
+
from env_ssl_wrapper import StandardizeEnvWrapper
|
|
335
|
+
|
|
336
|
+
env = StandardizeEnvWrapper(gym.make('CartPole-v1'))
|
|
337
|
+
|
|
338
|
+
stats = evaluate_actor(lambda obs: torch.zeros(obs.shape[0], 2), env, episodes = 10, seed = 0, verbose = True)
|
|
339
|
+
|
|
340
|
+
print(stats.summary())
|
|
@@ -34,6 +34,7 @@ from .adapters import (
|
|
|
34
34
|
BaseEnvAdapter,
|
|
35
35
|
get_adapter,
|
|
36
36
|
register_adapter,
|
|
37
|
+
MjlabAdapter,
|
|
37
38
|
MujocoWarpAdapter,
|
|
38
39
|
IsaacAdapter,
|
|
39
40
|
PyBulletAdapter,
|
|
@@ -68,6 +69,7 @@ from .mocks import (
|
|
|
68
69
|
AutoresetVectorMockEnv,
|
|
69
70
|
PufferVectorMockEnv,
|
|
70
71
|
PufferTensorMockEnv,
|
|
72
|
+
MjlabMockEnv,
|
|
71
73
|
ManiSkillMockEnv,
|
|
72
74
|
BraxMockEnv,
|
|
73
75
|
MjxMockEnv,
|
|
@@ -121,6 +123,7 @@ __all__ = [
|
|
|
121
123
|
'BaseEnvAdapter',
|
|
122
124
|
'get_adapter',
|
|
123
125
|
'register_adapter',
|
|
126
|
+
'MjlabAdapter',
|
|
124
127
|
'MujocoWarpAdapter',
|
|
125
128
|
'IsaacAdapter',
|
|
126
129
|
'PyBulletAdapter',
|
|
@@ -149,6 +152,7 @@ __all__ = [
|
|
|
149
152
|
'AutoresetVectorMockEnv',
|
|
150
153
|
'PufferVectorMockEnv',
|
|
151
154
|
'PufferTensorMockEnv',
|
|
155
|
+
'MjlabMockEnv',
|
|
152
156
|
'ManiSkillMockEnv',
|
|
153
157
|
'BraxMockEnv',
|
|
154
158
|
'MjxMockEnv',
|
|
@@ -129,8 +129,6 @@ class ActionTransformWrapper(EnvWrapper):
|
|
|
129
129
|
if not is_float_dtype(t):
|
|
130
130
|
return t
|
|
131
131
|
|
|
132
|
-
t = copy_leaf(t)
|
|
133
|
-
|
|
134
132
|
for ind, transform in enumerate(self.transforms):
|
|
135
133
|
indices = transform.get('indices', ind if len(self.transforms) > 1 else None)
|
|
136
134
|
|
|
@@ -141,6 +139,9 @@ class ActionTransformWrapper(EnvWrapper):
|
|
|
141
139
|
fn = partial(rescale, from_range = from_range, to_range = to_range)
|
|
142
140
|
|
|
143
141
|
if exists(indices):
|
|
142
|
+
# copy only when writing in place, so the caller's action is never mutated
|
|
143
|
+
|
|
144
|
+
t = copy_leaf(t)
|
|
144
145
|
t[..., indices] = fn(t[..., indices])
|
|
145
146
|
else:
|
|
146
147
|
t = fn(t)
|
|
@@ -6,18 +6,20 @@ from .helpers import (
|
|
|
6
6
|
exists,
|
|
7
7
|
first_existing,
|
|
8
8
|
get_attr,
|
|
9
|
-
is_time_step,
|
|
10
9
|
normalize_reset_out,
|
|
11
10
|
normalize_step_out,
|
|
12
11
|
safe_close,
|
|
13
12
|
truthy_attr,
|
|
14
|
-
zero_like,
|
|
15
13
|
)
|
|
16
14
|
from .spaces import space_from_action_spec
|
|
17
15
|
|
|
18
16
|
# base adapter
|
|
19
17
|
|
|
20
18
|
class BaseEnvAdapter:
|
|
19
|
+
# torch-native sims consume torch actions directly on device — no numpy roundtrip
|
|
20
|
+
|
|
21
|
+
torch_native = False
|
|
22
|
+
|
|
21
23
|
@classmethod
|
|
22
24
|
def matches(cls, env) -> bool:
|
|
23
25
|
return False
|
|
@@ -148,6 +150,10 @@ class WrapperAdapter(BaseEnvAdapter):
|
|
|
148
150
|
val = first_existing(self.env, 'autoreset', 'autoresets', 'autoreset_mode')
|
|
149
151
|
return truthy_attr(val) if exists(val) else self.inner_adapter.autoresets
|
|
150
152
|
|
|
153
|
+
@property
|
|
154
|
+
def torch_native(self) -> bool:
|
|
155
|
+
return self.inner_adapter.torch_native
|
|
156
|
+
|
|
151
157
|
@property
|
|
152
158
|
def action_space(self):
|
|
153
159
|
return default(
|
|
@@ -194,13 +200,8 @@ class DMControlAdapter(BaseEnvAdapter):
|
|
|
194
200
|
def action_space(self):
|
|
195
201
|
return default(super().action_space, space_from_action_spec(self.env))
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return False
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def autoresets(self) -> bool:
|
|
203
|
-
return False
|
|
203
|
+
is_vectorized = False
|
|
204
|
+
autoresets = False
|
|
204
205
|
|
|
205
206
|
# pybullet adapter
|
|
206
207
|
|
|
@@ -227,17 +228,32 @@ class PyBulletAdapter(BaseEnvAdapter):
|
|
|
227
228
|
return
|
|
228
229
|
super().seed(seed)
|
|
229
230
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
is_vectorized = False
|
|
232
|
+
autoresets = False
|
|
233
|
+
|
|
234
|
+
# mjlab adapter (manager-based rl envs on mujoco warp)
|
|
235
|
+
|
|
236
|
+
class MjlabAdapter(BaseEnvAdapter):
|
|
237
|
+
torch_native = True
|
|
238
|
+
is_vectorized = True
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def matches(cls, env):
|
|
242
|
+
mod = getattr(type(env), '__module__', '')
|
|
243
|
+
name = type(env).__name__
|
|
244
|
+
return 'mjlab' in mod.lower() or name == 'ManagerBasedRlEnv'
|
|
233
245
|
|
|
234
246
|
@property
|
|
235
247
|
def autoresets(self) -> bool:
|
|
236
|
-
return
|
|
248
|
+
return truthy_attr(get_attr(get_attr(self.env, 'cfg'), 'auto_reset', True))
|
|
237
249
|
|
|
238
250
|
# isaac sim adapter (isaac gym / isaac lab / omniverse)
|
|
239
251
|
|
|
240
252
|
class IsaacAdapter(BaseEnvAdapter):
|
|
253
|
+
torch_native = True
|
|
254
|
+
is_vectorized = True
|
|
255
|
+
autoresets = True
|
|
256
|
+
|
|
241
257
|
@classmethod
|
|
242
258
|
def matches(cls, env):
|
|
243
259
|
mod = getattr(type(env), '__module__', '')
|
|
@@ -245,14 +261,6 @@ class IsaacAdapter(BaseEnvAdapter):
|
|
|
245
261
|
isaac_kw = ('isaac', 'omni.isaac', 'isaacgym', 'isaaclab')
|
|
246
262
|
return any(k in mod.lower() for k in isaac_kw) or 'Isaac' in name or exists(get_attr(env, 'sim_device')) or exists(get_attr(env, 'physics_sim_view'))
|
|
247
263
|
|
|
248
|
-
@property
|
|
249
|
-
def is_vectorized(self) -> bool:
|
|
250
|
-
return True
|
|
251
|
-
|
|
252
|
-
@property
|
|
253
|
-
def autoresets(self) -> bool:
|
|
254
|
-
return True
|
|
255
|
-
|
|
256
264
|
# mujoco warp / warp / brax / mjx adapter
|
|
257
265
|
|
|
258
266
|
class MujocoWarpAdapter(BaseEnvAdapter):
|
|
@@ -276,16 +284,14 @@ class MujocoWarpAdapter(BaseEnvAdapter):
|
|
|
276
284
|
# pufferlib adapter
|
|
277
285
|
|
|
278
286
|
class PufferLibAdapter(BaseEnvAdapter):
|
|
287
|
+
is_vectorized = True
|
|
288
|
+
|
|
279
289
|
@classmethod
|
|
280
290
|
def matches(cls, env):
|
|
281
291
|
mod = getattr(type(env), '__module__', '')
|
|
282
292
|
name = type(env).__name__
|
|
283
293
|
return 'Puffer' in name or 'pufferlib' in mod or exists(get_attr(env, 'puffer_env'))
|
|
284
294
|
|
|
285
|
-
@property
|
|
286
|
-
def is_vectorized(self) -> bool:
|
|
287
|
-
return True
|
|
288
|
-
|
|
289
295
|
@property
|
|
290
296
|
def autoresets(self) -> bool:
|
|
291
297
|
return truthy_attr(first_existing(self.env, 'autoreset', 'autoresets', 'autoreset_mode'))
|
|
@@ -350,6 +356,18 @@ class GymnasiumAdapter(BaseEnvAdapter):
|
|
|
350
356
|
pass
|
|
351
357
|
return self.num_envs > 1
|
|
352
358
|
|
|
359
|
+
def unwrapped_adapter(self):
|
|
360
|
+
# foreign gym wrappers (TimeLimit, RecordEpisodeStatistics, ...) inherit
|
|
361
|
+
# the unwrapped sim's adapter traits
|
|
362
|
+
|
|
363
|
+
inner = get_attr(self.env, 'unwrapped')
|
|
364
|
+
return get_adapter(inner) if exists(inner) and inner is not self.env else None
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def torch_native(self) -> bool:
|
|
368
|
+
inner = self.unwrapped_adapter()
|
|
369
|
+
return exists(inner) and inner.torch_native
|
|
370
|
+
|
|
353
371
|
@property
|
|
354
372
|
def autoresets(self) -> bool:
|
|
355
373
|
mode = first_existing(self.env, 'autoreset', 'autoresets', 'autoreset_mode')
|
|
@@ -359,9 +377,13 @@ class GymnasiumAdapter(BaseEnvAdapter):
|
|
|
359
377
|
|
|
360
378
|
try:
|
|
361
379
|
from gymnasium.vector import VectorEnv
|
|
362
|
-
|
|
380
|
+
if isinstance(self.env, VectorEnv):
|
|
381
|
+
return True
|
|
363
382
|
except ImportError:
|
|
364
|
-
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
inner = self.unwrapped_adapter()
|
|
386
|
+
return exists(inner) and inner.autoresets
|
|
365
387
|
|
|
366
388
|
def seed(self, seed: int):
|
|
367
389
|
try:
|
|
@@ -400,6 +422,7 @@ class DefaultAdapter(BaseEnvAdapter):
|
|
|
400
422
|
|
|
401
423
|
ADAPTER_REGISTRY: list[type[BaseEnvAdapter]] = [
|
|
402
424
|
WrapperAdapter,
|
|
425
|
+
MjlabAdapter,
|
|
403
426
|
MujocoWarpAdapter,
|
|
404
427
|
IsaacAdapter,
|
|
405
428
|
PyBulletAdapter,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from types import SimpleNamespace
|
|
3
4
|
from typing import NamedTuple
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
@@ -391,6 +392,52 @@ class IsaacLabMockEnv(MockEnv):
|
|
|
391
392
|
|
|
392
393
|
return self.obs(), torch.randn(self.num_envs), dones, torch.zeros(self.num_envs, dtype = torch.bool), {}
|
|
393
394
|
|
|
395
|
+
# mjlab — manager-based RL on mujoco warp: always vectorized, torch-native
|
|
396
|
+
# (step takes torch tensors only), autoresets within step, dict obs of torch tensors
|
|
397
|
+
|
|
398
|
+
class MjlabMockEnv(MockEnv):
|
|
399
|
+
num_envs = 4
|
|
400
|
+
is_vector = True
|
|
401
|
+
is_vector_env = True
|
|
402
|
+
|
|
403
|
+
def __init__(self, num_envs = 4, auto_reset = True, seed = 0):
|
|
404
|
+
self.num_envs = num_envs
|
|
405
|
+
self.cfg = SimpleNamespace(auto_reset = auto_reset)
|
|
406
|
+
super().__init__(seed)
|
|
407
|
+
|
|
408
|
+
def reset(self, *, seed = None, env_ids = None, options = None):
|
|
409
|
+
if seed is not None:
|
|
410
|
+
self.seed(seed)
|
|
411
|
+
else:
|
|
412
|
+
self.reset_state()
|
|
413
|
+
return self.obs(), {}
|
|
414
|
+
|
|
415
|
+
def obs(self):
|
|
416
|
+
return dict(
|
|
417
|
+
policy = torch.randn(self.num_envs, self.obs_dim),
|
|
418
|
+
critic = torch.randn(self.num_envs, self.obs_dim * 2)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def step(self, action):
|
|
422
|
+
if not is_tensor(action):
|
|
423
|
+
raise TypeError(f'mjlab envs step on torch tensors, got {type(action).__name__}')
|
|
424
|
+
|
|
425
|
+
self.advance(action)
|
|
426
|
+
self.last_action = action
|
|
427
|
+
|
|
428
|
+
dones = torch.from_numpy(self.is_done())
|
|
429
|
+
|
|
430
|
+
if self.cfg.auto_reset:
|
|
431
|
+
for ind in np.where(dones.numpy())[0]:
|
|
432
|
+
self.t[ind] = 0
|
|
433
|
+
self.state[ind] = 0
|
|
434
|
+
|
|
435
|
+
return self.obs(), torch.ones(self.num_envs), dones, torch.zeros(self.num_envs, dtype = torch.bool), {}
|
|
436
|
+
|
|
437
|
+
# live in the real module path so the adapter recognizes the mock
|
|
438
|
+
|
|
439
|
+
MjlabMockEnv.__module__ = 'mjlab.envs.manager_based_rl_env'
|
|
440
|
+
|
|
394
441
|
# maniskill — gymnasium-compliant, but always batched even at num_envs = 1:
|
|
395
442
|
# obs / reward / dones are torch tensors with a leading batch dim, and the
|
|
396
443
|
# env exposes single_action_space / single_observation_space (unbatched)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from torch.utils._pytree import tree_map
|
|
5
5
|
|
|
6
|
-
from .helpers import default, exists,
|
|
6
|
+
from .helpers import default, exists, get_attr, to_numpy
|
|
7
7
|
|
|
8
8
|
# stand-in for environments that do not expose gymnasium spaces
|
|
9
9
|
|
{env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/tensor_wrapper.py
RENAMED
|
@@ -12,6 +12,7 @@ from .helpers import (
|
|
|
12
12
|
FINAL_OBSERVATION_KEYS,
|
|
13
13
|
FINAL_OBSERVATION_MASK_KEYS,
|
|
14
14
|
exists,
|
|
15
|
+
get_adapter,
|
|
15
16
|
get_attr,
|
|
16
17
|
is_scalar,
|
|
17
18
|
unpack_vector_observations,
|
|
@@ -96,6 +97,11 @@ class TensorWrapper(EnvWrapper):
|
|
|
96
97
|
self.convert_in = convert_in
|
|
97
98
|
self.convert_out = convert_out
|
|
98
99
|
self.cast_obs_to_float = cast_obs_to_float
|
|
100
|
+
|
|
101
|
+
# torch-native sims (mjlab, isaac) take torch actions as-is — zero-copy passthrough
|
|
102
|
+
|
|
103
|
+
self.torch_native = get_adapter(env).torch_native
|
|
104
|
+
|
|
99
105
|
self.cast = partial(numpy_to_torch, device = self.device, cast_obs_to_float = self.cast_obs_to_float)
|
|
100
106
|
|
|
101
107
|
def cast_info(self, info):
|
|
@@ -126,7 +132,9 @@ class TensorWrapper(EnvWrapper):
|
|
|
126
132
|
return contract(self.cast(t), to_float = to_float)
|
|
127
133
|
|
|
128
134
|
def step(self, action):
|
|
129
|
-
|
|
135
|
+
if self.convert_in and not self.torch_native:
|
|
136
|
+
action = torch_to_numpy(action)
|
|
137
|
+
|
|
130
138
|
obs, reward, terminated, truncated, info = self.env.step(action)
|
|
131
139
|
|
|
132
140
|
if not self.convert_out:
|
|
@@ -4,7 +4,7 @@ from multiprocessing import get_context
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import torch
|
|
6
6
|
from torch import is_tensor
|
|
7
|
-
from torch.utils._pytree import
|
|
7
|
+
from torch.utils._pytree import tree_map
|
|
8
8
|
|
|
9
9
|
from .helpers import (
|
|
10
10
|
any_true,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "env-ssl-wrapper"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.1"
|
|
4
4
|
description = "One torch-native interface for any MDP environment"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Phil Wang", email = "lucidrains@gmail.com" }
|
|
@@ -27,6 +27,8 @@ dependencies = [
|
|
|
27
27
|
"discrete-continuous-embed-readout",
|
|
28
28
|
"einx>=0.3.0",
|
|
29
29
|
"einops>=0.8.1",
|
|
30
|
+
"imageio",
|
|
31
|
+
"imageio-ffmpeg",
|
|
30
32
|
"memmap-replay-buffer",
|
|
31
33
|
"torch>=2.5",
|
|
32
34
|
"torch-einops-utils>=0.0.29",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/done_tracker_wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|
{env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|
{env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/image_wrapper.py
RENAMED
|
File without changes
|
{env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/standardize_wrapper.py
RENAMED
|
File without changes
|
{env_ssl_wrapper-0.4.7 → env_ssl_wrapper-0.5.1}/env_ssl_wrapper/standardize/time_limit_wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|