env-ssl-wrapper 0.5.1__tar.gz → 0.5.2__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.
Files changed (26) hide show
  1. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/PKG-INFO +25 -7
  2. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/README.md +23 -0
  3. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/__init__.py +7 -11
  4. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/evaluate.py +55 -7
  5. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/memory_trace.py +5 -2
  6. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/__init__.py +1 -0
  7. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/action_transform_wrapper.py +13 -8
  8. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/auto_batched_wrapper.py +8 -18
  9. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/episode_padding_wrapper.py +1 -2
  10. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py +2 -11
  11. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/helpers.py +2 -3
  12. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/image_wrapper.py +1 -5
  13. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/tensor_wrapper.py +17 -5
  14. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/vector.py +3 -1
  15. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/pyproject.toml +2 -7
  16. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/.gitignore +0 -0
  17. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/LICENSE +0 -0
  18. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/action_chunk.py +0 -0
  19. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/adapters.py +0 -0
  20. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/done_tracker_wrapper.py +0 -0
  21. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/mocks.py +0 -0
  22. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/spaces.py +0 -0
  23. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/standardize_env_wrapper.py +0 -0
  24. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/standardize_wrapper.py +0 -0
  25. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/env_ssl_wrapper/standardize/time_limit_wrapper.py +0 -0
  26. {env_ssl_wrapper-0.5.1 → env_ssl_wrapper-0.5.2}/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.5.1
3
+ Version: 0.5.2
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
@@ -34,16 +34,11 @@ Classifier: License :: OSI Approved :: MIT License
34
34
  Classifier: Programming Language :: Python :: 3.10
35
35
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
36
36
  Requires-Python: >=3.10
37
- Requires-Dist: discrete-continuous-embed-readout
38
37
  Requires-Dist: einops>=0.8.1
39
- Requires-Dist: einx>=0.3.0
40
38
  Requires-Dist: imageio
41
39
  Requires-Dist: imageio-ffmpeg
42
- Requires-Dist: memmap-replay-buffer
43
- Requires-Dist: torch-einops-utils>=0.0.29
40
+ Requires-Dist: numpy>=1.24
44
41
  Requires-Dist: torch>=2.5
45
- Requires-Dist: x-mlps-pytorch
46
- Requires-Dist: x-transformers
47
42
  Provides-Extra: examples
48
43
  Requires-Dist: fire; extra == 'examples'
49
44
  Provides-Extra: test
@@ -277,6 +272,29 @@ with MultiprocessingVecEnv('CartPole-v1', num_envs = 8) as env:
277
272
  obs, info = env.reset()
278
273
  ```
279
274
 
275
+ ## Evaluation
276
+
277
+ `evaluate_actor` rolls a policy through any standardized env and returns per-episode stats, optionally recording videos.
278
+
279
+ ```python
280
+ import gymnasium as gym
281
+ from env_ssl_wrapper import StandardizeEnvWrapper, evaluate_actor
282
+
283
+ env = StandardizeEnvWrapper(gym.make('CartPole-v1'))
284
+
285
+ stats = evaluate_actor(policy, env, episodes = 10, seed = 0)
286
+
287
+ stats.mean # mean return
288
+ stats.std # std of returns
289
+ stats.min, stats.max
290
+ stats.success_rate(475.) # fraction of episodes above a threshold
291
+ stats.summary() # dict(episodes, mean, std, min, max)
292
+
293
+ # deterministic = False samples from distributions instead of collapsing to the mode
294
+ # an actor exposing `.dist` takes precedence over calling it directly
295
+ # video_path = 'eval.mp4' records the first video_episodes episodes (single env only)
296
+ ```
297
+
280
298
  ## Tests
281
299
 
282
300
  ```bash
@@ -212,6 +212,29 @@ with MultiprocessingVecEnv('CartPole-v1', num_envs = 8) as env:
212
212
  obs, info = env.reset()
213
213
  ```
214
214
 
215
+ ## Evaluation
216
+
217
+ `evaluate_actor` rolls a policy through any standardized env and returns per-episode stats, optionally recording videos.
218
+
219
+ ```python
220
+ import gymnasium as gym
221
+ from env_ssl_wrapper import StandardizeEnvWrapper, evaluate_actor
222
+
223
+ env = StandardizeEnvWrapper(gym.make('CartPole-v1'))
224
+
225
+ stats = evaluate_actor(policy, env, episodes = 10, seed = 0)
226
+
227
+ stats.mean # mean return
228
+ stats.std # std of returns
229
+ stats.min, stats.max
230
+ stats.success_rate(475.) # fraction of episodes above a threshold
231
+ stats.summary() # dict(episodes, mean, std, min, max)
232
+
233
+ # deterministic = False samples from distributions instead of collapsing to the mode
234
+ # an actor exposing `.dist` takes precedence over calling it directly
235
+ # video_path = 'eval.mp4' records the first video_episodes episodes (single env only)
236
+ ```
237
+
215
238
  ## Tests
216
239
 
217
240
  ```bash
@@ -1,18 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+
4
5
  from . import standardize
5
- from .standardize import *
6
+ from .standardize import * # noqa: F403
6
7
 
7
8
  from .memory_trace import MemoryTraceWrapper
8
9
  from .action_chunk import ActionChunkWrapper
9
10
  from .evaluate import evaluate_actor, EpisodeStats
10
11
 
11
- # Wire backwards-compatibility aliases in sys.modules and module globals
12
- # so imports like `from env_ssl_wrapper.done_tracker_wrapper import DoneTrackerWrapper`
13
- # or `import env_ssl_wrapper.mocks` continue to work without breaking.
12
+ # backwards compatibility — the standardize wrappers used to live at the top
13
+ # level, so keep the old module paths (e.g. `env_ssl_wrapper.mocks`) importable
14
14
 
15
- _STANDARDIZE_SUBMODULES = (
15
+ _LEGACY_MODULES = (
16
16
  'adapters',
17
17
  'auto_batched_wrapper',
18
18
  'action_transform_wrapper',
@@ -31,11 +31,8 @@ _STANDARDIZE_SUBMODULES = (
31
31
  'vector',
32
32
  )
33
33
 
34
- for _name in _STANDARDIZE_SUBMODULES:
35
- _mod = getattr(standardize, _name, None)
36
- if _mod is not None:
37
- sys.modules[f'{__name__}.{_name}'] = _mod
38
- globals()[_name] = _mod
34
+ for _name in _LEGACY_MODULES:
35
+ sys.modules[f'{__name__}.{_name}'] = getattr(standardize, _name)
39
36
 
40
37
  __all__ = [
41
38
  *standardize.__all__,
@@ -49,4 +46,3 @@ def __getattr__(name):
49
46
  if hasattr(standardize, name):
50
47
  return getattr(standardize, name)
51
48
  raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
52
-
@@ -9,6 +9,7 @@ from torch import nn, is_tensor
9
9
  from torch.distributions import Categorical, Distribution
10
10
  from torch.utils._pytree import tree_flatten, tree_map
11
11
 
12
+ from .standardize.spaces import action_space_is_discrete
12
13
  from .standardize.helpers import (
13
14
  EnvWrapper,
14
15
  env_autoresets,
@@ -93,7 +94,19 @@ def to_action(env, out, deterministic = True):
93
94
  if not deterministic:
94
95
  return out.sample()
95
96
 
96
- return out.probs.argmax(dim = -1) if exists(get_attr(out, 'probs')) else out.mean
97
+ probs = get_attr(out, 'probs')
98
+
99
+ # categorical-style probs carry a trailing action axis to argmax over
100
+
101
+ if exists(probs) and probs.ndim >= 2 and probs.shape[-1] > 1:
102
+ return probs.argmax(dim = -1)
103
+
104
+ # bernoulli-style probabilities threshold at 0.5
105
+
106
+ if exists(probs):
107
+ return (probs > 0.5).long()
108
+
109
+ return out.mean
97
110
 
98
111
  if is_tensor(out) and out.is_floating_point() and out.ndim > 0:
99
112
  num_actions = get_attr(get_adapter(env).action_space, 'n')
@@ -136,10 +149,21 @@ def to_env_action(env, action):
136
149
 
137
150
  action = to_numpy(action)
138
151
 
139
- if action.ndim > 1 and action.shape[0] == 1:
140
- action = action[0]
152
+ # single raw sims get the leading batch dim stripped, while discrete
153
+ # actions additionally collapse to scalars
154
+
155
+ if not is_vectorized(env):
156
+ if action.ndim > 1 and action.shape[0] == 1:
157
+ action = action[0]
158
+
159
+ if action_space_is_discrete(get_adapter(env).action_space):
160
+ if action.ndim > 0 and action.shape[0] == 1:
161
+ action = action[0]
162
+
163
+ if action.ndim == 0:
164
+ action = action.item()
141
165
 
142
- return action.item() if action.ndim == 0 else action
166
+ return action
143
167
 
144
168
  # wrappers take batched torch actions - pad up to the declared rank
145
169
 
@@ -169,6 +193,9 @@ def capture_frame(env):
169
193
  except Exception:
170
194
  return None
171
195
 
196
+ if not exists(frame):
197
+ return None
198
+
172
199
  if is_tensor(frame):
173
200
  frame = frame.detach().cpu().numpy()
174
201
 
@@ -200,6 +227,17 @@ def video_path_at(path, index):
200
227
  path = Path(path)
201
228
  return str(path if index == 0 else path.with_name(f'{path.stem}_ep{index}{path.suffix}'))
202
229
 
230
+ def safe_reset(env, seed = None):
231
+ # gymnasium resets accept seed; legacy sims reset without it
232
+
233
+ if exists(seed):
234
+ try:
235
+ return normalize_reset_out(env.reset(seed = seed))
236
+ except TypeError:
237
+ pass
238
+
239
+ return normalize_reset_out(env.reset())
240
+
203
241
  # evaluate
204
242
 
205
243
  @torch.no_grad()
@@ -241,7 +279,7 @@ def evaluate_actor(
241
279
 
242
280
  assert not (record and num_envs > 1), 'video recording requires a single env'
243
281
 
244
- obs, info = normalize_reset_out(env.reset(seed = seed) if exists(seed) else env.reset())
282
+ obs, info = safe_reset(env, seed)
245
283
  batched = is_vectorized(env) or all(map(is_tensor, tree_flatten(obs)[0]))
246
284
 
247
285
  goal = goal.expand(num_envs, -1) if exists(goal) and goal.shape[0] == 1 and num_envs > 1 else goal
@@ -249,6 +287,7 @@ def evaluate_actor(
249
287
  returns = torch.zeros(num_envs)
250
288
  lengths = torch.zeros(num_envs)
251
289
  steps = torch.zeros(num_envs, dtype = torch.long)
290
+ active = np.ones(num_envs, dtype = bool)
252
291
 
253
292
  done_returns, done_lengths, videos, frames = [], [], [], []
254
293
 
@@ -285,7 +324,12 @@ def evaluate_actor(
285
324
  if exists(max_steps):
286
325
  done |= to_numpy(steps >= max_steps).reshape(-1)
287
326
 
288
- for i in np.where(done)[0]:
327
+ # a slot is only recorded once per episode — non-autoreset slots
328
+ # latch their done flag until the whole vector env resets
329
+
330
+ newly_done = done & active
331
+
332
+ for i in np.where(newly_done)[0]:
289
333
  if len(done_returns) >= episodes:
290
334
  break
291
335
 
@@ -309,6 +353,7 @@ def evaluate_actor(
309
353
  returns[i] = 0.
310
354
  lengths[i] = 0.
311
355
  steps[i] = 0
356
+ active[i] = False
312
357
 
313
358
  if done.any() and len(done_returns) < episodes:
314
359
  if record:
@@ -318,7 +363,10 @@ def evaluate_actor(
318
363
 
319
364
  if not env_autoresets(env) and (num_envs == 1 or done.all() or bool(get_attr(env, 'needs_reset', False))):
320
365
  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())
366
+ obs, info = safe_reset(env, reset_seed)
367
+ active[:] = True
368
+ elif env_autoresets(env):
369
+ active[newly_done] = True
322
370
  finally:
323
371
  if exists(module) and exists(was_training):
324
372
  module.train(was_training)
@@ -3,7 +3,6 @@ from typing import Sequence
3
3
 
4
4
  import torch
5
5
  from torch import is_tensor
6
- from torch_einops_utils import pad_right_ndim_to
7
6
 
8
7
  from .standardize.helpers import (
9
8
  TransformObservationWrapper,
@@ -27,7 +26,11 @@ def calc_lerp_weight(lam, done, x):
27
26
  done = torch.as_tensor(done, device = x.device)
28
27
 
29
28
  weight = torch.where(done, 1., 1. - lam).to(x)
30
- return pad_right_ndim_to(weight, x.ndim)
29
+
30
+ if x.ndim <= weight.ndim:
31
+ return weight
32
+
33
+ return weight.reshape(*weight.shape, *((1,) * (x.ndim - weight.ndim)))
31
34
 
32
35
  # Partially Observable Reinforcement Learning with Memory Traces - Eberhard et al.
33
36
  # https://arxiv.org/abs/2503.15200
@@ -120,6 +120,7 @@ __all__ = [
120
120
  'MultiprocessingVecEnv',
121
121
  'StandardizeEnvWrapper',
122
122
  'StandardizeEnv',
123
+ 'StandardizedEnv',
123
124
  'BaseEnvAdapter',
124
125
  'get_adapter',
125
126
  'register_adapter',
@@ -5,7 +5,6 @@ import torch
5
5
  from torch import is_tensor
6
6
  from torch.utils._pytree import tree_map
7
7
  from functools import partial
8
- from einops import rearrange
9
8
 
10
9
  from .helpers import EnvWrapper, copy_leaf, default, exists, first_existing, get_attr
11
10
  from .spaces import space_from_action_spec
@@ -95,7 +94,7 @@ class ActionTransformWrapper(EnvWrapper):
95
94
  was_scalar = t.ndim == 0
96
95
 
97
96
  if was_scalar:
98
- t = rearrange(t, '-> 1')
97
+ t = t[None]
99
98
 
100
99
  # bounds may be unbatched (single_action_space) or already batched
101
100
  # (vector envs exposing only a batched action_space)
@@ -105,22 +104,28 @@ class ActionTransformWrapper(EnvWrapper):
105
104
  low = np.broadcast_to(low, (dim,))
106
105
  high = np.broadcast_to(high, (dim,))
107
106
 
107
+ # invalid (unbounded) dims pass through — neutral bounds keep inf out of the arithmetic
108
+
108
109
  valid = np.isfinite(low) & np.isfinite(high)
109
110
 
110
111
  if is_tensor(t):
111
- low = torch.tensor(low, device = t.device, dtype = t.dtype)
112
- high = torch.tensor(high, device = t.device, dtype = t.dtype)
113
- valid = torch.tensor(valid, device = t.device)
112
+ valid_t = torch.as_tensor(valid, device = t.device)
113
+ low = torch.as_tensor(np.where(valid, low, 0.), device = t.device, dtype = t.dtype)
114
+ high = torch.as_tensor(np.where(valid, high, 1.), device = t.device, dtype = t.dtype)
115
+ else:
116
+ valid_t = valid
117
+ low = np.where(valid, low, 0.)
118
+ high = np.where(valid, high, 1.)
114
119
 
115
120
  rescaled = rescale(t, self.from_range, (low, high))
116
121
 
117
122
  if is_tensor(t):
118
- rescaled = torch.where(valid, torch.clamp(rescaled, low, high), t)
123
+ rescaled = torch.where(valid_t, torch.clamp(rescaled, low, high), t)
119
124
  else:
120
- rescaled = np.where(valid, np.clip(rescaled, low, high), t)
125
+ rescaled = np.where(valid_t, np.clip(rescaled, low, high), t)
121
126
 
122
127
  if was_scalar:
123
- rescaled = rearrange(rescaled, '1 ->')
128
+ rescaled = rescaled[0]
124
129
 
125
130
  return rescaled
126
131
 
@@ -5,7 +5,6 @@ from functools import partial
5
5
  import numpy as np
6
6
  import torch
7
7
  from torch.utils._pytree import tree_map
8
- from einops import rearrange
9
8
 
10
9
  from .helpers import (
11
10
  FINAL_OBSERVATION_KEYS,
@@ -40,22 +39,12 @@ def maybe_expand_dim(x):
40
39
 
41
40
  def _expand(t):
42
41
  arr = to_numeric_array(t)
42
+ return arr[None] if is_array(arr) else t
43
43
 
44
- if not is_array(arr):
45
- return t
44
+ if is_array(x):
45
+ return x[None]
46
46
 
47
- if arr.ndim == 0:
48
- return rearrange(arr, '-> 1')
49
-
50
- return rearrange(arr, '... -> 1 ...')
51
-
52
- if is_tensor(x):
53
- return rearrange(x, '-> 1') if x.ndim == 0 else rearrange(x, '... -> 1 ...')
54
-
55
- if isinstance(x, np.ndarray) and x.dtype.kind in 'biufc':
56
- return rearrange(x, '-> 1') if x.ndim == 0 else rearrange(x, '... -> 1 ...')
57
-
58
- return tree_map(_expand, x)
47
+ return tree_map(_expand, x) if isinstance(x, (dict, list, tuple)) else _expand(x)
59
48
 
60
49
  def is_integer_dtype(t):
61
50
  if is_tensor(t):
@@ -120,11 +109,11 @@ def heuristic_leaf(t, is_vector = False):
120
109
  return arr
121
110
 
122
111
  if not is_vector and arr.ndim > 1 and arr.shape[0] == 1:
123
- arr = rearrange(arr, '1 ... -> ...')
112
+ arr = arr.reshape(*arr.shape[1:])
124
113
 
125
114
  if is_integer_dtype(arr):
126
115
  while arr.ndim > 1 and arr.shape[-1] == 1:
127
- arr = rearrange(arr, '... 1 -> ...')
116
+ arr = arr.reshape(*arr.shape[:-1])
128
117
 
129
118
  if not is_vector and (arr.numel() if is_tensor(arr) else arr.size) == 1:
130
119
  return arr.item()
@@ -206,7 +195,8 @@ class AutoBatchedWrapper(EnvWrapper):
206
195
  if self.is_vector:
207
196
  return out
208
197
 
209
- obs, reward, terminated, truncated, info = *maybe_expand_dim(out[:4]), out[4]
198
+ obs, reward, terminated, truncated, info = out
199
+ obs, reward, terminated, truncated = (maybe_expand_dim(t) for t in (obs, reward, terminated, truncated))
210
200
 
211
201
  if isinstance(info, dict):
212
202
  for key in FINAL_OBSERVATION_KEYS:
@@ -6,7 +6,6 @@ import numpy as np
6
6
  import torch
7
7
  from torch import is_tensor
8
8
  from torch.utils._pytree import tree_map
9
- from einops import rearrange
10
9
 
11
10
  from .helpers import (
12
11
  EnvWrapper,
@@ -28,7 +27,7 @@ def broadcast_mask(mask, x):
28
27
  m = torch.as_tensor(mask, device = x.device, dtype = torch.bool)
29
28
  diff = x.ndim - m.ndim
30
29
 
31
- return rearrange(m, f'... -> ... {" ".join(["1"] * diff)}') if diff > 0 else m
30
+ return m.reshape(*m.shape, *((1,) * diff)) if diff > 0 else m
32
31
 
33
32
  def zero_mask(x, mask, fill_scalar = None):
34
33
  if is_tensor(x):
@@ -4,7 +4,6 @@ import numpy as np
4
4
  import torch
5
5
  from torch import is_tensor
6
6
  from torch.utils._pytree import tree_flatten
7
- from einops import rearrange
8
7
 
9
8
  from .helpers import (
10
9
  TransformObservationWrapper,
@@ -30,17 +29,9 @@ def flatten_leaf(t, is_vector = False):
30
29
  t = np.asarray(t)
31
30
 
32
31
  if not is_vector:
33
- if t.ndim == 0:
34
- return rearrange(t, '-> 1')
35
- return rearrange(t, '... -> (...)')
32
+ return t.reshape(1) if t.ndim == 0 else t.reshape(-1)
36
33
 
37
- if t.ndim == 0:
38
- return rearrange(t, '-> 1 1')
39
-
40
- if t.ndim == 1:
41
- return rearrange(t, 'b -> b 1')
42
-
43
- return rearrange(t, 'b ... -> b (...)')
34
+ return t.reshape(1, 1) if t.ndim == 0 else t.reshape(t.shape[0], -1)
44
35
 
45
36
  def concat_leaves(leaves, is_vector = False):
46
37
  axis = -1 if is_vector else 0
@@ -84,9 +84,6 @@ def copy_leaf(x):
84
84
 
85
85
  return x
86
86
 
87
- def copy_tree(tree):
88
- return tree_map(copy_leaf, tree)
89
-
90
87
  def dones_of(terminated, truncated):
91
88
  if not isinstance(terminated, (dict, list, tuple)):
92
89
  return terminated | truncated
@@ -138,6 +135,8 @@ def _zero_leaf(x):
138
135
  return False
139
136
  if isinstance(x, (int, float, np.number)):
140
137
  return np.zeros_like(x)
138
+ if exists(get_attr(x, '__array__')):
139
+ return np.zeros_like(np.asarray(x))
141
140
  return 0
142
141
 
143
142
  def _stack_leaves(leaves):
@@ -39,14 +39,10 @@ def render_frame(env, image_size = (64, 64), camera = None):
39
39
 
40
40
  img = env.render()
41
41
 
42
- if isinstance(img, (list, tuple)):
43
- img = np.ascontiguousarray(img)
44
-
45
42
  if isinstance(img, torch.Tensor):
46
43
  img = img.detach().cpu().numpy()
47
44
 
48
- img = np.ascontiguousarray(img)
49
- img = torch.from_numpy(img)
45
+ img = torch.from_numpy(np.ascontiguousarray(img))
50
46
 
51
47
  if img.ndim == 4:
52
48
  return rearrange(img, 'b h w c -> b c h w'), True
@@ -32,6 +32,10 @@ def to_torch_leaf(t, device, cast_obs_to_float = True):
32
32
  return t
33
33
 
34
34
  dtype = torch.float32 if cast_obs_to_float and t.dtype != torch.bool else t.dtype
35
+
36
+ if t.dtype == dtype and t.device == device:
37
+ return t
38
+
35
39
  return t.to(device = device, dtype = dtype)
36
40
 
37
41
  def numpy_to_torch(x, device, cast_obs_to_float = True):
@@ -72,7 +76,11 @@ def torch_to_numpy(x):
72
76
  def contract(t, to_float = False):
73
77
  if not is_tensor(t):
74
78
  return t
75
- return t.float() if to_float else t.bool()
79
+
80
+ if to_float:
81
+ return t if t.dtype == torch.float32 else t.float()
82
+
83
+ return t if t.dtype == torch.bool else t.bool()
76
84
 
77
85
  # class
78
86
 
@@ -105,7 +113,7 @@ class TensorWrapper(EnvWrapper):
105
113
  self.cast = partial(numpy_to_torch, device = self.device, cast_obs_to_float = self.cast_obs_to_float)
106
114
 
107
115
  def cast_info(self, info):
108
- if not isinstance(info, dict):
116
+ if not isinstance(info, dict) or not info:
109
117
  return
110
118
 
111
119
  for key in FINAL_OBSERVATION_KEYS:
@@ -126,10 +134,14 @@ class TensorWrapper(EnvWrapper):
126
134
  return obs, info
127
135
 
128
136
  def to_contract(self, t, to_float = False):
129
- if not isinstance(t, (dict, list, tuple)):
130
- leaf = to_torch_leaf(t, self.device, cast_obs_to_float = False)
137
+ def convert(leaf):
138
+ leaf = to_torch_leaf(leaf, self.device, cast_obs_to_float = False)
131
139
  return contract(leaf, to_float = to_float)
132
- return contract(self.cast(t), to_float = to_float)
140
+
141
+ if not isinstance(t, (dict, list, tuple)):
142
+ return convert(t)
143
+
144
+ return tree_map(convert, t)
133
145
 
134
146
  def step(self, action):
135
147
  if self.convert_in and not self.torch_native:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from multiprocessing import get_context
4
4
  import numpy as np
5
- import torch
6
5
  from torch import is_tensor
7
6
  from torch.utils._pytree import tree_map
8
7
 
@@ -119,6 +118,9 @@ class _InlineConn:
119
118
  def recv(self):
120
119
  return self._result
121
120
 
121
+ def close(self):
122
+ pass
123
+
122
124
  def _safe_send(conn, msg):
123
125
  """Send that swallows broken-pipe — for broadcast patterns where _recv_all reports the error."""
124
126
  try:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "env-ssl-wrapper"
3
- version = "0.5.1"
3
+ version = "0.5.2"
4
4
  description = "One torch-native interface for any MDP environment"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
@@ -24,16 +24,11 @@ classifiers=[
24
24
  ]
25
25
 
26
26
  dependencies = [
27
- "discrete-continuous-embed-readout",
28
- "einx>=0.3.0",
29
27
  "einops>=0.8.1",
30
28
  "imageio",
31
29
  "imageio-ffmpeg",
32
- "memmap-replay-buffer",
30
+ "numpy>=1.24",
33
31
  "torch>=2.5",
34
- "torch-einops-utils>=0.0.29",
35
- "x-transformers",
36
- "x-mlps-pytorch",
37
32
  ]
38
33
 
39
34
  [project.urls]
File without changes