env-ssl-wrapper 0.5.3__tar.gz → 0.5.4__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.3 → env_ssl_wrapper-0.5.4}/PKG-INFO +1 -1
  2. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/action_chunk.py +8 -3
  3. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/evaluate.py +58 -46
  4. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/memory_trace.py +21 -9
  5. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/action_transform_wrapper.py +2 -0
  6. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/auto_batched_wrapper.py +16 -19
  7. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/done_tracker_wrapper.py +2 -0
  8. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/episode_padding_wrapper.py +2 -0
  9. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py +5 -1
  10. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/helpers.py +31 -24
  11. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/image_wrapper.py +3 -3
  12. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/spaces.py +1 -1
  13. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/standardize_wrapper.py +2 -0
  14. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/tensor_wrapper.py +5 -11
  15. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/time_limit_wrapper.py +2 -0
  16. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/utils.py +10 -31
  17. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/vector.py +28 -16
  18. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/pyproject.toml +1 -1
  19. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/.gitignore +0 -0
  20. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/LICENSE +0 -0
  21. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/README.md +0 -0
  22. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/__init__.py +0 -0
  23. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/__init__.py +0 -0
  24. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/adapters.py +0 -0
  25. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/mocks.py +0 -0
  26. {env_ssl_wrapper-0.5.3 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/standardize_env_wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: env-ssl-wrapper
3
- Version: 0.5.3
3
+ Version: 0.5.4
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
@@ -11,6 +11,7 @@ from .standardize.helpers import (
11
11
  default,
12
12
  any_true,
13
13
  dones_of,
14
+ env_takes_torch,
14
15
  first_existing,
15
16
  get_adapter,
16
17
  get_attr,
@@ -29,16 +30,20 @@ def chunk_steps(actions, axis):
29
30
  def unbatch_step(action):
30
31
  # chunks always carry a num_envs axis - drop it for plain single envs
31
32
  if is_tensor(action):
32
- return action.squeeze(0)
33
+ action = action.squeeze(0)
34
+ return action.item() if action.ndim == 0 and not action.is_floating_point() else action
33
35
 
34
36
  if isinstance(action, np.ndarray):
35
- return np.squeeze(action, axis = 0) if action.ndim > 0 else action
37
+ action = np.squeeze(action, axis = 0) if action.ndim > 0 else action
38
+ return action.item() if action.ndim == 0 and np.issubdtype(action.dtype, np.integer) else action
36
39
 
37
40
  return action
38
41
 
39
42
  # wrapper
40
43
 
41
44
  class ActionChunkWrapper(EnvWrapper):
45
+ priority = 100
46
+
42
47
  """
43
48
  Open-loop action chunking - executes a chunk of actions in one step,
44
49
  temporally compressing the environment by the chunk length.
@@ -71,7 +76,7 @@ class ActionChunkWrapper(EnvWrapper):
71
76
  self.action_shape = tuple(get_attr(action_space, 'shape', ()) or ())
72
77
 
73
78
  self.expects_batch = is_vectorized(env)
74
- self.convert_in = not isinstance(env, EnvWrapper) and not get_adapter(env).torch_native
79
+ self.convert_in = not env_takes_torch(env)
75
80
 
76
81
  @property
77
82
  def chunk_action_shape(self):
@@ -12,9 +12,11 @@ from torch.utils._pytree import tree_flatten, tree_map
12
12
  from .standardize.spaces import action_space_is_discrete
13
13
  from .standardize.helpers import (
14
14
  EnvWrapper,
15
+ dones_of,
15
16
  env_autoresets,
16
17
  env_num_envs,
17
18
  env_render,
19
+ env_takes_torch,
18
20
  exists,
19
21
  first_existing,
20
22
  get_adapter,
@@ -90,31 +92,34 @@ def is_distribution(out):
90
92
  def to_action(env, out, deterministic = True):
91
93
  # distributions collapse to their mode, raw floating logits over a discrete space to their argmax
92
94
 
93
- if is_distribution(out):
94
- if not deterministic:
95
- return out.sample()
95
+ def convert_leaf(val):
96
+ if is_distribution(val):
97
+ if not deterministic:
98
+ return val.sample()
96
99
 
97
- probs = get_attr(out, 'probs')
100
+ probs = get_attr(val, 'probs')
98
101
 
99
- # categorical-style probs carry a trailing action axis to argmax over
102
+ # categorical-style probs carry a trailing action axis to argmax over
100
103
 
101
- if exists(probs) and probs.ndim >= 2 and probs.shape[-1] > 1:
102
- return probs.argmax(dim = -1)
104
+ if exists(probs) and probs.ndim >= 2 and probs.shape[-1] > 1:
105
+ return probs.argmax(dim = -1)
103
106
 
104
- # bernoulli-style probabilities threshold at 0.5
107
+ # bernoulli-style probabilities threshold at 0.5
105
108
 
106
- if exists(probs):
107
- return (probs > 0.5).long()
109
+ if exists(probs):
110
+ return (probs > 0.5).long()
108
111
 
109
- return out.mean
112
+ return val.mean
110
113
 
111
- if is_tensor(out) and out.is_floating_point() and out.ndim > 0:
112
- num_actions = get_attr(get_adapter(env).action_space, 'n')
114
+ if is_tensor(val) and val.is_floating_point() and val.ndim > 0:
115
+ num_actions = get_attr(get_adapter(env).action_space, 'n')
113
116
 
114
- if exists(num_actions) and out.shape[-1] == num_actions:
115
- return out.argmax(dim = -1) if deterministic else Categorical(logits = out).sample()
117
+ if exists(num_actions) and val.shape[-1] == num_actions:
118
+ return val.argmax(dim = -1) if deterministic else Categorical(logits = val).sample()
116
119
 
117
- return out
120
+ return val
121
+
122
+ return tree_map(convert_leaf, out)
118
123
 
119
124
  # states and actions
120
125
 
@@ -135,48 +140,55 @@ def to_actor_obs(obs, device, batched):
135
140
  return tree_map(convert, obs)
136
141
 
137
142
  def to_env_action(env, action):
138
- if not is_tensor(action):
139
- action = torch.as_tensor(action)
140
-
141
143
  device = get_attr(env, 'device')
142
- action = action.to(device) if exists(device) else action
143
144
 
144
- # raw simulators take unbatched numpy — torch-native sims take torch as-is (zero-copy)
145
+ def to_torch(a):
146
+ if not is_tensor(a):
147
+ a = torch.as_tensor(a)
148
+ return a.to(device) if exists(device) else a
145
149
 
146
- if not isinstance(env, EnvWrapper):
147
- if get_adapter(env).torch_native:
148
- return action
150
+ action = tree_map(to_torch, action)
149
151
 
150
- action = to_numpy(action)
152
+ chunk_shape = get_attr(env, 'chunk_action_shape')
151
153
 
152
- # single raw sims get the leading batch dim stripped, while discrete
153
- # actions additionally collapse to scalars
154
+ if exists(chunk_shape):
155
+ rank = len(chunk_shape) + 1
156
+ else:
157
+ space = first_existing(env, 'single_action_space', 'action_space')
158
+ shape = get_attr(space, 'shape')
159
+ rank = len(shape) + 1 if exists(shape) else None
154
160
 
155
- if not is_vectorized(env):
156
- if action.ndim > 1 and action.shape[0] == 1:
157
- action = action[0]
161
+ if exists(rank):
162
+ def pad_rank(a):
163
+ while a.ndim < rank:
164
+ a = a[None]
165
+ return a
158
166
 
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]
167
+ action = tree_map(pad_rank, action)
162
168
 
163
- if action.ndim == 0:
164
- action = action.item()
169
+ # raw simulators take unbatched numpy — torch-native sims take torch as-is (zero-copy)
165
170
 
166
- return action
171
+ if not env_takes_torch(env):
172
+ action = tree_map(to_numpy, action)
167
173
 
168
- # wrappers take batched torch actions - pad up to the declared rank
174
+ # single raw sims get the leading batch dim stripped, while discrete
175
+ # actions additionally collapse to scalars
169
176
 
170
- chunk_shape = get_attr(env, 'chunk_action_shape')
177
+ if not is_vectorized(env) and not exists(chunk_shape):
178
+ def strip_leaf(a):
179
+ if a.ndim > 1 and a.shape[0] == 1:
180
+ a = a[0]
171
181
 
172
- if exists(chunk_shape):
173
- rank = len(chunk_shape) + 1
174
- else:
175
- space = first_existing(env, 'single_action_space', 'action_space')
176
- rank = len(get_attr(space, 'shape', ())) + 1
182
+ if action_space_is_discrete(get_adapter(env).action_space):
183
+ if a.ndim > 0 and a.shape[0] == 1:
184
+ a = a[0]
185
+
186
+ if a.ndim == 0:
187
+ a = a.item()
188
+
189
+ return a
177
190
 
178
- while action.ndim < rank:
179
- action = action[None]
191
+ action = tree_map(strip_leaf, action)
180
192
 
181
193
  return action
182
194
 
@@ -319,7 +331,7 @@ def evaluate_actor(
319
331
  lengths += info.get('chunk_length', 1) if isinstance(info, dict) else 1
320
332
  steps += 1
321
333
 
322
- done = to_numpy(terminated | truncated).astype(bool).reshape(-1)
334
+ done = to_numpy(dones_of(terminated, truncated)).astype(bool).reshape(-1)
323
335
 
324
336
  if exists(max_steps):
325
337
  done |= to_numpy(steps >= max_steps).reshape(-1)
@@ -6,16 +6,16 @@ from torch import is_tensor
6
6
 
7
7
  from .standardize.helpers import (
8
8
  TransformObservationWrapper,
9
- exists,
10
- default,
11
9
  any_true,
10
+ cast_tuple,
11
+ default,
12
+ exists,
13
+ is_array_like,
14
+ is_scalar,
12
15
  )
13
16
 
14
17
  # helper functions
15
18
 
16
- def cast_tuple(val):
17
- return val if isinstance(val, (tuple, list)) else (val,)
18
-
19
19
  def calc_lerp_weight(lam, done, x):
20
20
  if not exists(done) or not any_true(done):
21
21
  return 1. - lam
@@ -23,7 +23,9 @@ def calc_lerp_weight(lam, done, x):
23
23
  # reset trace on done, else decay
24
24
 
25
25
  if not is_tensor(done):
26
- done = torch.as_tensor(done, device = x.device)
26
+ done = torch.as_tensor(done, device = x.device, dtype = torch.bool)
27
+ else:
28
+ done = done.to(device = x.device, dtype = torch.bool)
27
29
 
28
30
  weight = torch.where(done, 1., 1. - lam).to(x)
29
31
 
@@ -36,6 +38,7 @@ def calc_lerp_weight(lam, done, x):
36
38
  # https://arxiv.org/abs/2503.15200
37
39
 
38
40
  class MemoryTraceWrapper(TransformObservationWrapper):
41
+ priority = 80
39
42
 
40
43
  def __init__(
41
44
  self,
@@ -61,17 +64,26 @@ class MemoryTraceWrapper(TransformObservationWrapper):
61
64
  return f'{prefix}_{lam}' if len(self.lambdas) > 1 else prefix
62
65
 
63
66
  def transform_obs(self, obs, done = None):
64
- out = dict(obs) if isinstance(obs, dict) else {self.obs_key: obs}
67
+ if isinstance(obs, (tuple, list)):
68
+ out = {f'{self.obs_key}_{i}': elem for i, elem in enumerate(obs)}
69
+ elif isinstance(obs, dict):
70
+ out = dict(obs)
71
+ else:
72
+ out = {self.obs_key: obs}
73
+
65
74
  target_keys = tuple(k for k in default(self.keys, tuple(out.keys())) if k in out)
66
75
 
67
76
  for key in target_keys:
68
77
  val = out.get(key)
69
78
 
70
- if not exists(val):
79
+ if not exists(val) or not (is_array_like(val) or is_scalar(val)):
71
80
  continue
72
81
 
73
82
  if not is_tensor(val):
74
- val = torch.as_tensor(val)
83
+ try:
84
+ val = torch.as_tensor(val)
85
+ except (TypeError, ValueError):
86
+ continue
75
87
  out[key] = val
76
88
 
77
89
  val_float = val.float() if not val.is_floating_point() else val
@@ -57,6 +57,8 @@ def action_bounds(env):
57
57
  # wrapper
58
58
 
59
59
  class ActionTransformWrapper(EnvWrapper):
60
+ priority = 30
61
+
60
62
  def __init__(
61
63
  self,
62
64
  env,
@@ -4,7 +4,7 @@ from functools import partial
4
4
 
5
5
  import numpy as np
6
6
  import torch
7
- from torch.utils._pytree import tree_map
7
+ from torch.utils._pytree import tree_flatten, tree_map, tree_structure, tree_unflatten
8
8
 
9
9
  from .helpers import (
10
10
  FINAL_OBSERVATION_KEYS,
@@ -35,16 +35,11 @@ def to_numeric_array(t):
35
35
  return arr if arr.dtype.kind in 'biufc' else t
36
36
 
37
37
  def maybe_expand_dim(x):
38
- # add leading batch dim to every numeric leaf
39
-
40
38
  def _expand(t):
41
39
  arr = to_numeric_array(t)
42
40
  return arr[None] if is_array(arr) else t
43
41
 
44
- if is_array(x):
45
- return x[None]
46
-
47
- return tree_map(_expand, x) if isinstance(x, (dict, list, tuple)) else _expand(x)
42
+ return tree_map(_expand, x)
48
43
 
49
44
  def is_integer_dtype(t):
50
45
  if is_tensor(t):
@@ -122,20 +117,12 @@ def heuristic_leaf(t, is_vector = False):
122
117
  return arr.item() if arr.ndim == 0 else arr
123
118
 
124
119
  def rebuild_container(x, leaves):
125
-
126
- if isinstance(x, list):
127
- return leaves
128
-
129
- if exists(get_attr(type(x), '_fields')):
130
- return type(x)(*leaves)
131
-
132
- return type(x)(leaves)
120
+ return tree_unflatten(leaves, tree_structure(x))
133
121
 
134
122
  def is_numeric_container(x):
135
- # purely numeric nested sequences count as one leaf when no space declares structure
136
-
137
123
  if isinstance(x, (list, tuple)):
138
- return len(x) > 0 and all(is_numeric_container(item) for item in x)
124
+ leaves, _ = tree_flatten(x)
125
+ return len(leaves) > 0 and all(map(is_scalar, leaves))
139
126
 
140
127
  return is_scalar(x)
141
128
 
@@ -168,6 +155,7 @@ def maybe_squeeze_dim(x, shape_tree = None, is_vector = False, prepend_batch = F
168
155
  class AutoBatchedWrapper(EnvWrapper):
169
156
 
170
157
  is_auto_batched = True
158
+ priority = 50
171
159
 
172
160
  def __init__(self, env, is_vector: bool | None = None):
173
161
  super().__init__(env)
@@ -187,7 +175,16 @@ class AutoBatchedWrapper(EnvWrapper):
187
175
  if not exists(self.action_shape_tree):
188
176
  self.refresh_action_space()
189
177
 
190
- return (maybe_expand_dim(obs), info) if not self.is_vector else (obs, info)
178
+ if self.is_vector:
179
+ return obs, info
180
+
181
+ obs = maybe_expand_dim(obs)
182
+ if isinstance(info, dict):
183
+ for key in FINAL_OBSERVATION_KEYS:
184
+ if key in info:
185
+ info[key] = maybe_expand_dim(info[key])
186
+
187
+ return obs, info
191
188
 
192
189
  def step(self, action):
193
190
  action = maybe_squeeze_dim(action, shape_tree = self.action_shape_tree, is_vector = self.is_vector, prepend_batch = self.prepend_batch)
@@ -17,6 +17,8 @@ from .helpers import (
17
17
  # classes
18
18
 
19
19
  class DoneTrackerWrapper(EnvWrapper):
20
+ priority = 70
21
+
20
22
  def __init__(self, env):
21
23
  if not is_vectorized(env):
22
24
  env = AutoBatchedWrapper(env)
@@ -66,6 +66,8 @@ def merge_final(current, value, mask):
66
66
  # class
67
67
 
68
68
  class EpisodePaddingWrapper(EnvWrapper):
69
+ priority = 10
70
+
69
71
  def __init__(self, env, pad_autoreset: bool = True):
70
72
  super().__init__(env)
71
73
  self.is_vector = is_vectorized(env)
@@ -37,7 +37,9 @@ def concat_leaves(leaves, is_vector = False):
37
37
  axis = -1 if is_vector else 0
38
38
  leaves = [flatten_leaf(t, is_vector = is_vector) for t in leaves]
39
39
 
40
- if is_tensor(leaves[0]):
40
+ if any(map(is_tensor, leaves)):
41
+ device = next(t.device for t in leaves if is_tensor(t))
42
+ leaves = [torch.as_tensor(t, device = device) if not is_tensor(t) else t.to(device) for t in leaves]
41
43
  return torch.cat(leaves, dim = axis)
42
44
 
43
45
  return np.concatenate(leaves, axis = axis)
@@ -45,6 +47,8 @@ def concat_leaves(leaves, is_vector = False):
45
47
  # class
46
48
 
47
49
  class FlattenObsWrapper(TransformObservationWrapper):
50
+ priority = 90
51
+
48
52
  def __init__(self, env, is_vector: bool | None = None):
49
53
  super().__init__(env)
50
54
  self.is_vector = default(is_vector, is_vectorized(env))
@@ -12,7 +12,14 @@ def exists(v):
12
12
  return v is not None
13
13
 
14
14
  def default(v, d):
15
- return v if exists(v) else d
15
+ return v if exists(v) else (d() if callable(d) else d)
16
+
17
+ def cast_tuple(val, length = 1):
18
+ if isinstance(val, tuple):
19
+ return val
20
+ if isinstance(val, list):
21
+ return tuple(val)
22
+ return (val,) * length
16
23
 
17
24
  def get_attr(obj, name, default = None):
18
25
  # properties that raise count as missing
@@ -84,9 +91,8 @@ def to_numpy(t):
84
91
  return t.detach().cpu().numpy() if is_tensor(t) else np.asarray(t)
85
92
 
86
93
  def any_true(x):
87
- if is_tensor(x):
88
- return bool(x.any())
89
- return bool(np.asarray(x).any())
94
+ leaves, _ = tree_flatten(x)
95
+ return any(bool(leaf.any()) if is_tensor(leaf) else bool(np.asarray(leaf).any()) for leaf in leaves)
90
96
 
91
97
  def copy_leaf(x):
92
98
  if is_tensor(x):
@@ -98,8 +104,6 @@ def copy_leaf(x):
98
104
  return x
99
105
 
100
106
  def dones_of(terminated, truncated):
101
- if not isinstance(terminated, (dict, list, tuple)):
102
- return terminated | truncated
103
107
  return tree_map(lambda a, b: a | b, terminated, truncated)
104
108
 
105
109
  # sim step / reset normalization
@@ -107,13 +111,16 @@ def dones_of(terminated, truncated):
107
111
  def is_time_step(out):
108
112
  return exists(get_attr(out, 'step_type')) and exists(get_attr(out, 'observation'))
109
113
 
110
- def zero_like(x):
114
+ def _zero_bool_leaf(x):
111
115
  if is_tensor(x):
112
116
  return torch.zeros_like(x, dtype = torch.bool)
113
117
 
114
118
  arr = np.asarray(x)
115
119
  return np.zeros_like(arr, dtype = bool) if arr.ndim > 0 else False
116
120
 
121
+ def zero_like(x):
122
+ return tree_map(_zero_bool_leaf, x)
123
+
117
124
  def normalize_obs(obs):
118
125
  # raw arrays / scalars / text / containers pass through — custom observation
119
126
  # objects (rlbench, ...) flatten via get_low_dim_data or unpack their
@@ -148,16 +155,8 @@ def normalize_obs(obs):
148
155
  return out if out else obs
149
156
 
150
157
  def contains_array(x):
151
- # deep array-leaf check over nested containers — used to tell an obs tree
152
- # apart from a plain info dict when language instructions come first
153
-
154
- if isinstance(x, dict):
155
- return any(contains_array(v) for v in x.values())
156
-
157
- if isinstance(x, (list, tuple)):
158
- return any(contains_array(v) for v in x)
159
-
160
- return is_array_like(x)
158
+ leaves, _ = tree_flatten(x)
159
+ return any(map(is_array_like, leaves))
161
160
 
162
161
  def is_instruction(x):
163
162
  if isinstance(x, str):
@@ -235,15 +234,9 @@ def stack_trees(trees):
235
234
  if isinstance(first, np.ndarray):
236
235
  return np.stack(trees)
237
236
 
238
- if isinstance(first, dict):
239
- return {key: stack_trees([t[key] for t in trees]) for key in first}
240
-
241
- if isinstance(first, tuple):
242
- return tuple(stack_trees([t[i] for t in trees]) for i in range(len(first)))
243
-
244
237
  leaves = [tree_flatten(tree)[0] for tree in trees]
245
238
  stacked = [_stack_leaves(col) for col in zip(*leaves)]
246
- return tree_unflatten(stacked, tree_structure(trees[0]))
239
+ return tree_unflatten(stacked, tree_structure(first))
247
240
 
248
241
  def unpack_vector_observations(arr):
249
242
  # unpacks a 1D sequence / numpy object array of unbatched single-env observations
@@ -283,6 +276,18 @@ def env_render(env, height, width, camera = None):
283
276
  def is_vectorized(env) -> bool:
284
277
  return get_adapter(env).is_vectorized
285
278
 
279
+ def env_takes_torch(env) -> bool:
280
+ if get_adapter(env).torch_native:
281
+ return True
282
+
283
+ current = env
284
+ while isinstance(current, EnvWrapper):
285
+ if type(current).__name__ == 'TensorWrapper' and getattr(current, 'convert_in', True):
286
+ return True
287
+ current = getattr(current, 'env', None)
288
+
289
+ return False
290
+
286
291
  # gymnasium 1.x surfaces final observations as 'final_obs' / '_final_obs', everything here uses 'final_observation' / '_final_observation'
287
292
 
288
293
  FINAL_OBSERVATION_KEYS = ('final_observation', 'final_obs')
@@ -373,6 +378,8 @@ def safe_close(env):
373
378
  # base wrapper
374
379
 
375
380
  class EnvWrapper:
381
+ priority = 50
382
+
376
383
  def __init__(self, env):
377
384
  self.env = env
378
385
 
@@ -9,6 +9,7 @@ from einops import rearrange
9
9
  from .helpers import (
10
10
  FINAL_OBSERVATION_KEYS,
11
11
  TransformObservationWrapper,
12
+ cast_tuple,
12
13
  env_render,
13
14
  env_render_mode,
14
15
  exists,
@@ -18,9 +19,6 @@ from .helpers import (
18
19
 
19
20
  # helper functions
20
21
 
21
- def cast_tuple(t, length = 1):
22
- return t if isinstance(t, tuple) else ((t,) * length)
23
-
24
22
  def render_frame(env, image_size = (64, 64), camera = None):
25
23
  # render from any sim — the shared env_render probe knows each image
26
24
  # surface (dm_control physics, pybullet client, robosuite sim); envs
@@ -72,6 +70,8 @@ def process_image(
72
70
  # class
73
71
 
74
72
  class ImageObservationWrapper(TransformObservationWrapper):
73
+ priority = 20
74
+
75
75
  def __init__(
76
76
  self,
77
77
  env,
@@ -71,7 +71,7 @@ def space_dim(space) -> int | None:
71
71
  if exists(shape):
72
72
  return shape_dim(shape)
73
73
 
74
- subspaces = get_attr(space, 'spaces')
74
+ subspaces = space if isinstance(space, (dict, list, tuple)) else get_attr(space, 'spaces')
75
75
  if not exists(subspaces):
76
76
  return None
77
77
 
@@ -14,6 +14,8 @@ from .spaces import infer_observation_space, space_from_action_spec
14
14
  # class
15
15
 
16
16
  class StandardizeWrapper(EnvWrapper):
17
+ priority = 0
18
+
17
19
  # normalizes any sim into (obs, reward, terminated, truncated, info) via its adapter
18
20
 
19
21
  def __init__(self, env, adapter = None):
@@ -43,9 +43,6 @@ def numpy_to_torch(x, device, cast_obs_to_float = True):
43
43
  if isinstance(x, np.ndarray) and x.dtype == object:
44
44
  x = unpack_vector_observations(x)
45
45
 
46
- if not isinstance(x, (dict, list, tuple)):
47
- return to_torch_leaf(x, device, cast_obs_to_float)
48
-
49
46
  return tree_map(partial(to_torch_leaf, device = device, cast_obs_to_float = cast_obs_to_float), x)
50
47
 
51
48
  def to_numpy_leaf(t):
@@ -66,9 +63,6 @@ def to_numpy_leaf(t):
66
63
 
67
64
  def torch_to_numpy(x):
68
65
  # torch to numpy; 0-dim collapses to scalar, float64 → float32
69
- if not isinstance(x, (dict, list, tuple)):
70
- return to_numpy_leaf(x)
71
-
72
66
  return tree_map(to_numpy_leaf, x)
73
67
 
74
68
  # rewards float32, dones bool
@@ -85,6 +79,8 @@ def contract(t, to_float = False):
85
79
  # class
86
80
 
87
81
  class TensorWrapper(EnvWrapper):
82
+ priority = 60
83
+
88
84
  def __init__(
89
85
  self,
90
86
  env,
@@ -138,17 +134,15 @@ class TensorWrapper(EnvWrapper):
138
134
  leaf = to_torch_leaf(leaf, self.device, cast_obs_to_float = False)
139
135
  return contract(leaf, to_float = to_float)
140
136
 
141
- if not isinstance(t, (dict, list, tuple)):
142
- return convert(t)
143
-
144
137
  return tree_map(convert, t)
145
138
 
146
139
  def step(self, action):
147
140
  if self.convert_in:
148
141
  if not self.torch_native:
149
142
  action = torch_to_numpy(action)
150
- elif not is_tensor(action):
151
- # torch-native sims take torch on device — lift numpy / foreign actions
143
+ elif is_tensor(action):
144
+ action = action.to(self.device)
145
+ else:
152
146
  action = numpy_to_torch(action, device = self.device, cast_obs_to_float = False)
153
147
 
154
148
  obs, reward, terminated, truncated, info = self.env.step(action)
@@ -33,6 +33,8 @@ def back_to_like(t, numpy_arr):
33
33
  return numpy_arr
34
34
 
35
35
  class TimeLimitWrapper(EnvWrapper):
36
+ priority = 40
37
+
36
38
  """
37
39
  caps episode length at max_timesteps, setting truncated = True for
38
40
  capped envs (vectorized and single alike); timers reset per episode.
@@ -60,48 +60,27 @@ def parse_wrapper(wrapper):
60
60
  def compose_env(env, *wrappers, pad_episodes: bool = True):
61
61
  env = instantiate_env(env)
62
62
 
63
- funcs = []
64
- classes = []
65
-
66
- for wrapper in wrappers:
67
- func, cls = parse_wrapper(wrapper)
68
- funcs.append(func)
69
- classes.append(cls)
63
+ items = [parse_wrapper(wrapper) for wrapper in wrappers]
64
+ classes = {cls for _, cls in items}
70
65
 
71
66
  if StandardizeWrapper not in classes:
72
- funcs.insert(0, StandardizeWrapper)
73
- classes.insert(0, StandardizeWrapper)
67
+ items.append((StandardizeWrapper, StandardizeWrapper))
74
68
 
75
69
  # vectorized envs get standardized episode padding + a persistent final_observation
76
70
  # (autoresetting envs re-emit the true terminal obs, so no padding for those)
77
71
 
78
72
  if pad_episodes and EpisodePaddingWrapper not in classes and is_vectorized(env):
79
73
  pad_wrapper = partial(EpisodePaddingWrapper, pad_autoreset = not env_autoresets(env))
80
- funcs.insert(1, pad_wrapper)
81
- classes.insert(1, EpisodePaddingWrapper)
82
-
83
- from ..memory_trace import MemoryTraceWrapper
84
- if MemoryTraceWrapper in classes and TensorWrapper in classes:
85
- idx_mem = classes.index(MemoryTraceWrapper)
86
- idx_ten = classes.index(TensorWrapper)
87
- if idx_mem < idx_ten:
88
- f = funcs.pop(idx_mem)
89
- c = classes.pop(idx_mem)
90
- idx_ten = classes.index(TensorWrapper)
91
- funcs.insert(idx_ten + 1, f)
92
- classes.insert(idx_ten + 1, c)
93
-
94
- # action chunking changes the step signature, so it always goes outermost
95
-
96
- from ..action_chunk import ActionChunkWrapper
97
- if ActionChunkWrapper in classes:
98
- idx_chunk = classes.index(ActionChunkWrapper)
99
- funcs.append(funcs.pop(idx_chunk))
100
- classes.append(classes.pop(idx_chunk))
74
+ items.append((pad_wrapper, EpisodePaddingWrapper))
75
+
76
+ # sort by canonical pipeline priority (stable sort preserves user order for equal priorities)
77
+
78
+ items.sort(key = lambda item: getattr(item[1], 'priority', 50))
101
79
 
80
+ classes = [cls for _, cls in items]
102
81
  assert len(set(classes)) == len(classes), 'duplicate wrappers found'
103
82
 
104
- for func in funcs:
83
+ for func, _ in items:
105
84
  env = func(env)
106
85
 
107
86
  return env
@@ -3,20 +3,22 @@ from __future__ import annotations
3
3
  from multiprocessing import get_context
4
4
  import numpy as np
5
5
  from torch import is_tensor
6
- from torch.utils._pytree import tree_map
6
+ from torch.utils._pytree import tree_flatten, tree_map, tree_unflatten
7
7
 
8
8
  from .helpers import (
9
9
  any_true,
10
10
  dones_of,
11
11
  exists,
12
+ get_adapter,
12
13
  get_attr,
13
14
  instantiate_env,
14
15
  safe_close,
15
16
  stack_trees,
17
+ to_numpy,
16
18
  truthy_attr,
17
19
  _zero_leaf,
18
20
  )
19
- from .spaces import action_dim_of, obs_dim_of
21
+ from .spaces import action_dim_of, action_space_is_discrete, obs_dim_of
20
22
  from .standardize_wrapper import StandardizeWrapper
21
23
 
22
24
  # leaf helpers
@@ -48,6 +50,15 @@ class WorkerError(RuntimeError):
48
50
 
49
51
  def _exec(env, cmd, payload):
50
52
  if cmd == 'step':
53
+ if is_tensor(payload) and not get_adapter(env).torch_native:
54
+ payload = to_numpy(payload)
55
+
56
+ if action_space_is_discrete(env.action_space):
57
+ if hasattr(payload, 'item') and (not hasattr(payload, 'ndim') or payload.ndim <= 1):
58
+ payload = payload.item()
59
+ elif isinstance(payload, (list, tuple)) and len(payload) == 1:
60
+ payload = payload[0]
61
+
51
62
  obs, reward, terminated, truncated, info = env.step(payload)
52
63
 
53
64
  if any_true(dones_of(terminated, truncated)):
@@ -182,16 +193,14 @@ def _shutdown(conns, procs):
182
193
  pass
183
194
 
184
195
  def _split_actions(actions, num_envs):
185
- if isinstance(actions, dict):
186
- assert all(len(v) == num_envs for v in actions.values()), f'expected {num_envs} actions per key'
187
- return [{k: v[i] for k, v in actions.items()} for i in range(num_envs)]
188
-
189
- if isinstance(actions, tuple) and not is_tensor(actions) and not isinstance(actions, np.ndarray):
190
- assert all(len(v) == num_envs for v in actions), f'expected {num_envs} actions per element'
191
- return [tuple(elem[i] for elem in actions) for i in range(num_envs)]
196
+ if isinstance(actions, list) and len(actions) == num_envs:
197
+ return actions
192
198
 
193
- assert len(actions) == num_envs, f'expected {num_envs} actions, but got {len(actions)}'
194
- return actions
199
+ leaves, spec = tree_flatten(actions)
200
+ assert all(len(leaf) == num_envs for leaf in leaves), (
201
+ f'expected {num_envs} actions, but got {len(leaves[0]) if leaves else 0}'
202
+ )
203
+ return [tree_unflatten([leaf[i] for leaf in leaves], spec) for i in range(num_envs)]
195
204
 
196
205
  # class
197
206
 
@@ -270,16 +279,19 @@ class MultiprocessingVecEnv:
270
279
  _recv_all(self._conns)
271
280
 
272
281
  def reset(self, seed = None, **kwargs):
273
- if exists(seed):
274
- self.seed(seed)
275
-
276
- for conn in self._conns:
277
- _safe_send(conn, ('reset', kwargs))
282
+ for i, conn in enumerate(self._conns):
283
+ worker_kwargs = dict(kwargs)
284
+ if exists(seed):
285
+ worker_kwargs['seed'] = seed + i
286
+ _safe_send(conn, ('reset', worker_kwargs))
278
287
 
279
288
  results = _recv_all(self._conns)
280
289
  return stack_trees([obs for obs, _ in results]), _merge_infos([info for _, info in results])
281
290
 
282
291
  def step(self, actions):
292
+ if is_tensor(actions):
293
+ actions = to_numpy(actions)
294
+
283
295
  actions = _split_actions(actions, self.num_envs)
284
296
 
285
297
  for conn, action in zip(self._conns, actions):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "env-ssl-wrapper"
3
- version = "0.5.3"
3
+ version = "0.5.4"
4
4
  description = "One torch-native interface for any MDP environment"
5
5
  authors = [
6
6
  { name = "Phil Wang", email = "lucidrains@gmail.com" }
File without changes