env-ssl-wrapper 0.5.2__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.2 → env_ssl_wrapper-0.5.4}/PKG-INFO +1 -1
  2. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/action_chunk.py +8 -3
  3. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/evaluate.py +60 -48
  4. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/memory_trace.py +21 -9
  5. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/__init__.py +32 -0
  6. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/action_transform_wrapper.py +3 -1
  7. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/adapters.py +62 -21
  8. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/auto_batched_wrapper.py +22 -24
  9. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/done_tracker_wrapper.py +2 -0
  10. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/episode_padding_wrapper.py +7 -4
  11. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py +5 -1
  12. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/helpers.py +121 -33
  13. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/image_wrapper.py +4 -4
  14. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/mocks.py +249 -17
  15. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/spaces.py +25 -8
  16. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/standardize_wrapper.py +2 -0
  17. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/tensor_wrapper.py +12 -14
  18. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/time_limit_wrapper.py +6 -7
  19. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/utils.py +10 -31
  20. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/standardize/vector.py +29 -16
  21. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/pyproject.toml +1 -1
  22. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/.gitignore +0 -0
  23. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/LICENSE +0 -0
  24. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/README.md +0 -0
  25. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.4}/env_ssl_wrapper/__init__.py +0 -0
  26. {env_ssl_wrapper-0.5.2 → 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.2
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,14 +12,16 @@ 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,
21
23
  get_attr,
22
- is_array,
24
+ is_array_like,
23
25
  is_vectorized,
24
26
  normalize_reset_out,
25
27
  normalize_step_out,
@@ -90,37 +92,40 @@ 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
 
121
126
  def to_actor_obs(obs, device, batched):
122
127
  def convert(x):
123
- if not (is_array(x) or callable(get_attr(x, '__array__'))):
128
+ if not is_array_like(x):
124
129
  return x
125
130
 
126
131
  tensor = x if is_tensor(x) else torch.as_tensor(np.asarray(x))
@@ -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
@@ -77,6 +77,16 @@ from .mocks import (
77
77
  TrifingerMockEnv,
78
78
  HabitatMockEnv,
79
79
  TupleObsMockEnv,
80
+ RLBenchObservation,
81
+ RLBenchMockEnv,
82
+ OmniGibsonMockEnv,
83
+ RoboMimicMockEnv,
84
+ MyoSuiteMockEnv,
85
+ GymnasiumRoboticsMockEnv,
86
+ LeRobotMockEnv,
87
+ CalvinMockEnv,
88
+ PushTMockEnv,
89
+ DroneAviaryMockEnv,
80
90
  JaxArray,
81
91
  )
82
92
 
@@ -84,6 +94,12 @@ from .utils import wrap_env, compose_env
84
94
  from .helpers import (
85
95
  TransformObservationWrapper,
86
96
  ObservationWrapper,
97
+ is_array,
98
+ is_foreign_array,
99
+ is_array_like,
100
+ normalize_obs,
101
+ normalize_reset_out,
102
+ normalize_step_out,
87
103
  has_final_observation,
88
104
  maybe_get_final_observation,
89
105
  get_final_observation,
@@ -143,6 +159,12 @@ __all__ = [
143
159
  'action_space_bounds',
144
160
  'action_dim_of',
145
161
  'obs_dim_of',
162
+ 'is_array',
163
+ 'is_foreign_array',
164
+ 'is_array_like',
165
+ 'normalize_obs',
166
+ 'normalize_reset_out',
167
+ 'normalize_step_out',
146
168
  'MockEnv',
147
169
  'GymnasiumMockEnv',
148
170
  'GymnasiumDiscreteMockEnv',
@@ -161,6 +183,16 @@ __all__ = [
161
183
  'TrifingerMockEnv',
162
184
  'HabitatMockEnv',
163
185
  'TupleObsMockEnv',
186
+ 'RLBenchObservation',
187
+ 'RLBenchMockEnv',
188
+ 'OmniGibsonMockEnv',
189
+ 'RoboMimicMockEnv',
190
+ 'MyoSuiteMockEnv',
191
+ 'GymnasiumRoboticsMockEnv',
192
+ 'LeRobotMockEnv',
193
+ 'CalvinMockEnv',
194
+ 'PushTMockEnv',
195
+ 'DroneAviaryMockEnv',
164
196
  'JaxArray',
165
197
  'wrap_env',
166
198
  'compose_env',
@@ -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,
@@ -84,7 +86,7 @@ class ActionTransformWrapper(EnvWrapper):
84
86
  # rescale canonical range (default (0, 1), beta-friendly) to per-env
85
87
  # bounds, leaving unbounded dimensions untouched
86
88
 
87
- if self.bounds is None:
89
+ if not exists(self.bounds):
88
90
  return t
89
91
 
90
92
  low, high = self.bounds
@@ -13,6 +13,31 @@ from .helpers import (
13
13
  )
14
14
  from .spaces import space_from_action_spec
15
15
 
16
+ # helpers
17
+
18
+ def render_with_dims(render, height, width, camera = None):
19
+ # modern render(height, width, camera) vs legacy bare render()
20
+
21
+ try:
22
+ return render(height = height, width = width, camera = camera)
23
+ except TypeError:
24
+ return render()
25
+
26
+ def render_sim(sim, height, width, camera = None):
27
+ # sims vary — robosuite-style (camera_name) vs bare no-kwargs render
28
+
29
+ render = get_attr(sim, 'render')
30
+
31
+ if not callable(render):
32
+ return None
33
+
34
+ kwargs = dict(camera_name = camera) if exists(camera) else {}
35
+
36
+ try:
37
+ return render(height = height, width = width, **kwargs)
38
+ except TypeError:
39
+ return None
40
+
16
41
  # base adapter
17
42
 
18
43
  class BaseEnvAdapter:
@@ -58,13 +83,14 @@ class BaseEnvAdapter:
58
83
  _, _, rgba, _, _ = client.getCameraImage(width, height, renderer = renderer)
59
84
  return rgba[..., :3]
60
85
 
61
- sim = get_attr(self.env, 'sim')
62
- if exists(sim) and callable(get_attr(sim, 'render')):
63
- kwargs = dict(camera_name = camera) if exists(camera) else {}
64
- return sim.render(height = height, width = width, **kwargs)
86
+ frame = render_sim(get_attr(self.env, 'sim'), height, width, camera)
87
+ if exists(frame):
88
+ return frame
65
89
 
66
- if callable(get_attr(self.env, 'render')):
67
- return self.env.render()
90
+ render = get_attr(self.env, 'render')
91
+
92
+ if callable(render):
93
+ return render_with_dims(render, height, width, camera)
68
94
 
69
95
  return None
70
96
 
@@ -127,8 +153,11 @@ class WrapperAdapter(BaseEnvAdapter):
127
153
  self.inner_adapter.seed(seed)
128
154
 
129
155
  def render(self, height: int, width: int, camera = None):
130
- if callable(get_attr(self.env, 'render')):
131
- return self.env.render()
156
+ render = get_attr(self.env, 'render')
157
+
158
+ if callable(render):
159
+ return render_with_dims(render, height, width, camera)
160
+
132
161
  return self.inner_adapter.render(height, width, camera)
133
162
 
134
163
  def close(self):
@@ -296,25 +325,37 @@ class PufferLibAdapter(BaseEnvAdapter):
296
325
  def autoresets(self) -> bool:
297
326
  return truthy_attr(first_existing(self.env, 'autoreset', 'autoresets', 'autoreset_mode'))
298
327
 
299
- # robotics adapter (robosuite, maniskill, metaworld, trifinger, habitat)
328
+ # robotics adapter (robosuite, maniskill, metaworld, trifinger, habitat, rlbench,
329
+ # omnigibson, robomimic, myosuite, gymnasium-robotics, lerobot, ...)
330
+ # sits before dm_control — several robotics sims expose `physics` but are not dm_control
331
+
332
+ ROBOTICS_KEYWORDS = (
333
+ 'robosuite', 'mani_skill', 'maniskill', 'metaworld', 'trifinger', 'habitat',
334
+ 'omnigibson', 'igibson', 'robomimic', 'myosuite', 'gymnasiumrobotics',
335
+ 'gymnasium_robotics', 'gym_robotics', 'goalenv', 'lerobot',
336
+ 'gym_aloha', 'gym_pusht', 'aloha', 'pusht', 'sapien', 'rlbench', 'pyrep',
337
+ 'calvin', 'language_table', 'drone', 'aviary', 'softgym', 'plasticinelab'
338
+ )
300
339
 
301
340
  class RoboticsAdapter(BaseEnvAdapter):
302
341
  @classmethod
303
342
  def matches(cls, env):
304
- mod = getattr(type(env), '__module__', '')
305
- name = type(env).__name__
306
- robotics_names = ('Robosuite', 'ManiSkill', 'MetaWorld', 'Trifinger', 'Habitat')
307
- robotics_mods = ('robosuite', 'mani_skill', 'maniskill', 'metaworld', 'habitat')
343
+ mod = getattr(type(env), '__module__', '').lower()
344
+ name = type(env).__name__.lower()
308
345
  has_sim_render = exists(get_attr(env, 'sim')) and callable(get_attr(get_attr(env, 'sim'), 'render'))
309
- return any(r in name for r in robotics_names) or any(r in mod.lower() for r in robotics_mods) or has_sim_render
346
+ return any(k in name or k in mod for k in ROBOTICS_KEYWORDS) or has_sim_render
310
347
 
311
348
  def render(self, height: int, width: int, camera = None):
312
- sim = get_attr(self.env, 'sim')
313
- if exists(sim) and callable(get_attr(sim, 'render')):
314
- kwargs = dict(camera_name = camera) if exists(camera) else {}
315
- return sim.render(height = height, width = width, **kwargs)
316
- if callable(get_attr(self.env, 'render')):
317
- return self.env.render()
349
+ frame = render_sim(get_attr(self.env, 'sim'), height, width, camera)
350
+
351
+ if exists(frame):
352
+ return frame
353
+
354
+ render = get_attr(self.env, 'render')
355
+
356
+ if callable(render):
357
+ return render_with_dims(render, height, width, camera)
358
+
318
359
  return super().render(height, width, camera)
319
360
 
320
361
  @property
@@ -426,9 +467,9 @@ ADAPTER_REGISTRY: list[type[BaseEnvAdapter]] = [
426
467
  MujocoWarpAdapter,
427
468
  IsaacAdapter,
428
469
  PyBulletAdapter,
470
+ RoboticsAdapter,
429
471
  DMControlAdapter,
430
472
  PufferLibAdapter,
431
- RoboticsAdapter,
432
473
  GymnasiumAdapter,
433
474
  LegacyGymAdapter,
434
475
  DefaultAdapter,
@@ -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):
@@ -65,17 +60,18 @@ def get_action_space(env):
65
60
  return get_adapter(env).action_space, False
66
61
 
67
62
  def action_shape_tree(space):
68
- # canonical shapes parallel to action structure
63
+ # canonical shapes parallel to action structure — raw dict / list / tuple
64
+ # action spaces are handled as well as gymnasium's composite spaces
69
65
 
70
66
  if not exists(space):
71
67
  return None
72
68
 
73
- subspaces = get_attr(space, 'spaces')
69
+ subspaces = space if isinstance(space, (dict, list, tuple)) else get_attr(space, 'spaces')
74
70
 
75
- if exists(subspaces):
76
- if isinstance(subspaces, dict):
77
- return {key: action_shape_tree(subspace) for key, subspace in subspaces.items()}
71
+ if isinstance(subspaces, dict):
72
+ return {key: action_shape_tree(subspace) for key, subspace in subspaces.items()}
78
73
 
74
+ if exists(subspaces):
79
75
  return [action_shape_tree(subspace) for subspace in subspaces]
80
76
 
81
77
  return get_attr(space, 'shape')
@@ -121,20 +117,12 @@ def heuristic_leaf(t, is_vector = False):
121
117
  return arr.item() if arr.ndim == 0 else arr
122
118
 
123
119
  def rebuild_container(x, leaves):
124
-
125
- if isinstance(x, list):
126
- return leaves
127
-
128
- if exists(get_attr(type(x), '_fields')):
129
- return type(x)(*leaves)
130
-
131
- return type(x)(leaves)
120
+ return tree_unflatten(leaves, tree_structure(x))
132
121
 
133
122
  def is_numeric_container(x):
134
- # purely numeric nested sequences count as one leaf when no space declares structure
135
-
136
123
  if isinstance(x, (list, tuple)):
137
- 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))
138
126
 
139
127
  return is_scalar(x)
140
128
 
@@ -167,6 +155,7 @@ def maybe_squeeze_dim(x, shape_tree = None, is_vector = False, prepend_batch = F
167
155
  class AutoBatchedWrapper(EnvWrapper):
168
156
 
169
157
  is_auto_batched = True
158
+ priority = 50
170
159
 
171
160
  def __init__(self, env, is_vector: bool | None = None):
172
161
  super().__init__(env)
@@ -186,7 +175,16 @@ class AutoBatchedWrapper(EnvWrapper):
186
175
  if not exists(self.action_shape_tree):
187
176
  self.refresh_action_space()
188
177
 
189
- 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
190
188
 
191
189
  def step(self, action):
192
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)
@@ -36,7 +36,7 @@ def zero_mask(x, mask, fill_scalar = None):
36
36
  arr = np.asarray(x)
37
37
 
38
38
  if arr.ndim == 0:
39
- if fill_scalar is not None and bool(np.asarray(mask).any()):
39
+ if exists(fill_scalar) and bool(np.asarray(mask).any()):
40
40
  return fill_scalar
41
41
  return x
42
42
 
@@ -51,6 +51,7 @@ def back_to_mask_type(dones, newly):
51
51
 
52
52
  def merge_final(current, value, mask):
53
53
  if is_tensor(current):
54
+ value = torch.as_tensor(value, device = current.device, dtype = current.dtype)
54
55
  return torch.where(broadcast_mask(mask, current), value, current)
55
56
 
56
57
  curr = np.asarray(current)
@@ -59,12 +60,14 @@ def merge_final(current, value, mask):
59
60
  return current
60
61
 
61
62
  out = curr.copy()
62
- out[mask] = np.asarray(value)[mask]
63
+ out[mask] = to_numpy(value)[mask]
63
64
  return out
64
65
 
65
66
  # class
66
67
 
67
68
  class EpisodePaddingWrapper(EnvWrapper):
69
+ priority = 10
70
+
68
71
  def __init__(self, env, pad_autoreset: bool = True):
69
72
  super().__init__(env)
70
73
  self.is_vector = is_vectorized(env)
@@ -90,7 +93,7 @@ class EpisodePaddingWrapper(EnvWrapper):
90
93
  dones = dones_of(terminated, truncated)
91
94
  mask = to_numpy(dones).astype(bool)
92
95
 
93
- if self._is_done is None or len(self._is_done) != len(mask):
96
+ if not exists(self._is_done) or len(self._is_done) != len(mask):
94
97
  self._is_done = np.zeros(len(mask), dtype = bool)
95
98
 
96
99
  # autoreset envs revive every done slot, so each done is a new
@@ -106,7 +109,7 @@ class EpisodePaddingWrapper(EnvWrapper):
106
109
  final_val = first_existing(info, 'final_observation', 'final_obs')
107
110
  value = final_val if exists(final_val) else self._last_obs
108
111
 
109
- if self._final_obs is None:
112
+ if not exists(self._final_obs):
110
113
  self._final_obs = tree_map(copy_leaf, value)
111
114
  else:
112
115
  self._final_obs = tree_map(partial(merge_final, mask = newly), self._final_obs, value)
@@ -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))