env-ssl-wrapper 0.5.2__tar.gz → 0.5.3__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.3}/PKG-INFO +1 -1
  2. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/evaluate.py +2 -2
  3. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/__init__.py +32 -0
  4. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/action_transform_wrapper.py +1 -1
  5. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/adapters.py +62 -21
  6. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/auto_batched_wrapper.py +6 -5
  7. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/episode_padding_wrapper.py +5 -4
  8. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/helpers.py +100 -19
  9. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/image_wrapper.py +1 -1
  10. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/mocks.py +249 -17
  11. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/spaces.py +25 -8
  12. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/tensor_wrapper.py +9 -5
  13. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/time_limit_wrapper.py +4 -7
  14. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/vector.py +2 -1
  15. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/pyproject.toml +1 -1
  16. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/.gitignore +0 -0
  17. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/LICENSE +0 -0
  18. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/README.md +0 -0
  19. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/__init__.py +0 -0
  20. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/action_chunk.py +0 -0
  21. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/memory_trace.py +0 -0
  22. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/done_tracker_wrapper.py +0 -0
  23. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py +0 -0
  24. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/standardize_env_wrapper.py +0 -0
  25. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/env_ssl_wrapper/standardize/standardize_wrapper.py +0 -0
  26. {env_ssl_wrapper-0.5.2 → env_ssl_wrapper-0.5.3}/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.2
3
+ Version: 0.5.3
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
@@ -19,7 +19,7 @@ from .standardize.helpers import (
19
19
  first_existing,
20
20
  get_adapter,
21
21
  get_attr,
22
- is_array,
22
+ is_array_like,
23
23
  is_vectorized,
24
24
  normalize_reset_out,
25
25
  normalize_step_out,
@@ -120,7 +120,7 @@ def to_action(env, out, deterministic = True):
120
120
 
121
121
  def to_actor_obs(obs, device, batched):
122
122
  def convert(x):
123
- if not (is_array(x) or callable(get_attr(x, '__array__'))):
123
+ if not is_array_like(x):
124
124
  return x
125
125
 
126
126
  tensor = x if is_tensor(x) else torch.as_tensor(np.asarray(x))
@@ -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',
@@ -84,7 +84,7 @@ class ActionTransformWrapper(EnvWrapper):
84
84
  # rescale canonical range (default (0, 1), beta-friendly) to per-env
85
85
  # bounds, leaving unbounded dimensions untouched
86
86
 
87
- if self.bounds is None:
87
+ if not exists(self.bounds):
88
88
  return t
89
89
 
90
90
  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,
@@ -65,17 +65,18 @@ def get_action_space(env):
65
65
  return get_adapter(env).action_space, False
66
66
 
67
67
  def action_shape_tree(space):
68
- # canonical shapes parallel to action structure
68
+ # canonical shapes parallel to action structure — raw dict / list / tuple
69
+ # action spaces are handled as well as gymnasium's composite spaces
69
70
 
70
71
  if not exists(space):
71
72
  return None
72
73
 
73
- subspaces = get_attr(space, 'spaces')
74
+ subspaces = space if isinstance(space, (dict, list, tuple)) else get_attr(space, 'spaces')
74
75
 
75
- if exists(subspaces):
76
- if isinstance(subspaces, dict):
77
- return {key: action_shape_tree(subspace) for key, subspace in subspaces.items()}
76
+ if isinstance(subspaces, dict):
77
+ return {key: action_shape_tree(subspace) for key, subspace in subspaces.items()}
78
78
 
79
+ if exists(subspaces):
79
80
  return [action_shape_tree(subspace) for subspace in subspaces]
80
81
 
81
82
  return get_attr(space, 'shape')
@@ -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,7 +60,7 @@ 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
@@ -90,7 +91,7 @@ class EpisodePaddingWrapper(EnvWrapper):
90
91
  dones = dones_of(terminated, truncated)
91
92
  mask = to_numpy(dones).astype(bool)
92
93
 
93
- if self._is_done is None or len(self._is_done) != len(mask):
94
+ if not exists(self._is_done) or len(self._is_done) != len(mask):
94
95
  self._is_done = np.zeros(len(mask), dtype = bool)
95
96
 
96
97
  # autoreset envs revive every done slot, so each done is a new
@@ -106,7 +107,7 @@ class EpisodePaddingWrapper(EnvWrapper):
106
107
  final_val = first_existing(info, 'final_observation', 'final_obs')
107
108
  value = final_val if exists(final_val) else self._last_obs
108
109
 
109
- if self._final_obs is None:
110
+ if not exists(self._final_obs):
110
111
  self._final_obs = tree_map(copy_leaf, value)
111
112
  else:
112
113
  self._final_obs = tree_map(partial(merge_final, mask = newly), self._final_obs, value)
@@ -59,14 +59,27 @@ def get_batch_size(tree) -> int | None:
59
59
 
60
60
  first = leaves[0]
61
61
 
62
- if is_array(first):
63
- return len(first) if first.ndim > 0 else None
62
+ if isinstance(first, (str, bytes)):
63
+ return None
64
+
65
+ ndim = get_attr(first, 'ndim')
66
+
67
+ if exists(ndim):
68
+ return len(first) if ndim > 0 else None
64
69
 
65
70
  return len(first) if exists(get_attr(first, '__len__')) else None
66
71
 
67
72
  def is_array(v):
68
73
  return is_tensor(v) or isinstance(v, np.ndarray)
69
74
 
75
+ def is_foreign_array(v):
76
+ # array-like types from foreign frameworks (e.g. jax.Array, pil Image)
77
+ # that implement numpy's __array__ conversion protocol
78
+ return not is_array(v) and callable(get_attr(v, '__array__'))
79
+
80
+ def is_array_like(v):
81
+ return is_array(v) or is_foreign_array(v)
82
+
70
83
  def to_numpy(t):
71
84
  return t.detach().cpu().numpy() if is_tensor(t) else np.asarray(t)
72
85
 
@@ -101,28 +114,97 @@ def zero_like(x):
101
114
  arr = np.asarray(x)
102
115
  return np.zeros_like(arr, dtype = bool) if arr.ndim > 0 else False
103
116
 
117
+ def normalize_obs(obs):
118
+ # raw arrays / scalars / text / containers pass through — custom observation
119
+ # objects (rlbench, ...) flatten via get_low_dim_data or unpack their
120
+ # public array, scalar, or container attributes
121
+
122
+ if is_array_like(obs) or is_scalar(obs) or isinstance(obs, (str, bytes, dict, list, tuple)):
123
+ return obs
124
+
125
+ low_dim = get_attr(obs, 'get_low_dim_data')
126
+
127
+ if callable(low_dim):
128
+ try:
129
+ flattened = low_dim()
130
+ if exists(flattened):
131
+ return flattened
132
+ except Exception:
133
+ pass
134
+
135
+ out = {}
136
+ for key in dir(obs):
137
+ if key.startswith('_'):
138
+ continue
139
+
140
+ value = get_attr(obs, key)
141
+
142
+ if callable(value):
143
+ continue
144
+
145
+ if is_array_like(value) or is_scalar(value) or isinstance(value, (dict, list, tuple)):
146
+ out[key] = value
147
+
148
+ return out if out else obs
149
+
150
+ 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)
161
+
162
+ def is_instruction(x):
163
+ if isinstance(x, str):
164
+ return True
165
+
166
+ return isinstance(x, (list, tuple)) and len(x) > 0 and all(isinstance(d, str) for d in x)
167
+
168
+ def as_descriptions(x):
169
+ return [x] if isinstance(x, str) else list(x)
170
+
104
171
  def normalize_reset_out(out):
105
172
  if is_time_step(out):
106
- return out.observation, {}
173
+ return normalize_obs(out.observation), {}
174
+
175
+ if isinstance(out, tuple):
176
+ if len(out) == 2:
177
+ first, second = out
178
+
179
+ # language-conditioned robotics (e.g. rlbench, calvin, language-table):
180
+ # (instruction(s), obs) — a plain info dict in second means text observation
107
181
 
108
- if isinstance(out, tuple) and len(out) == 2:
109
- obs, info = out
110
- return obs, {} if info is None else (info if isinstance(info, dict) else {})
182
+ if is_instruction(first) and (not isinstance(second, dict) or contains_array(second)):
183
+ return normalize_obs(second), dict(descriptions = as_descriptions(first))
111
184
 
112
- return out, {}
185
+ info = second if isinstance(second, dict) else {}
186
+ return normalize_obs(first), info
187
+
188
+ if len(out) == 3 and is_instruction(out[0]):
189
+ obs = normalize_obs(out[1])
190
+ info = out[2] if isinstance(out[2], dict) else {}
191
+ return obs, {**info, 'descriptions': as_descriptions(out[0])}
192
+
193
+ return normalize_obs(out), {}
113
194
 
114
195
  def normalize_step_out(out):
115
196
  if is_time_step(out):
116
197
  last = out.last() if callable(get_attr(out, 'last')) else out.step_type == 2
117
- return out.observation, out.reward, last, False, dict(discount = out.discount)
198
+ return normalize_obs(out.observation), out.reward, last, False, dict(discount = out.discount)
118
199
 
119
200
  if len(out) == 5:
120
- return out
201
+ obs, reward, terminated, truncated, info = out
202
+ return normalize_obs(obs), reward, terminated, truncated, info if isinstance(info, dict) else {}
121
203
 
122
204
  if len(out) in (3, 4):
123
205
  obs, reward, done, *rest = out
124
206
  info = rest[0] if rest and isinstance(rest[0], dict) else {}
125
- return obs, reward, done, zero_like(done), info
207
+ return normalize_obs(obs), reward, done, zero_like(done), info
126
208
 
127
209
  raise ValueError(f'could not standardize step output of length {len(out)}')
128
210
 
@@ -135,7 +217,7 @@ def _zero_leaf(x):
135
217
  return False
136
218
  if isinstance(x, (int, float, np.number)):
137
219
  return np.zeros_like(x)
138
- if exists(get_attr(x, '__array__')):
220
+ if is_foreign_array(x):
139
221
  return np.zeros_like(np.asarray(x))
140
222
  return 0
141
223
 
@@ -173,11 +255,11 @@ def unpack_vector_observations(arr):
173
255
  if isinstance(arr, np.ndarray) and arr.dtype != object:
174
256
  return arr
175
257
 
176
- sample = next((x for x in arr if x is not None), None)
177
- if sample is None:
258
+ sample = next((x for x in arr if exists(x)), None)
259
+ if not exists(sample):
178
260
  return arr
179
261
 
180
- trees = [tree_map(_zero_leaf, sample) if x is None else x for x in arr]
262
+ trees = [tree_map(_zero_leaf, sample) if not exists(x) else x for x in arr]
181
263
  return stack_trees(trees)
182
264
 
183
265
  # environment probes
@@ -336,12 +418,12 @@ class TransformObservationWrapper(EnvWrapper):
336
418
 
337
419
  def reset(self, **kwargs):
338
420
  obs, info = self.env.reset(**kwargs)
339
- obs = self.transform_obs(obs)
421
+ obs = self.transform(obs, done = None)
340
422
 
341
423
  if isinstance(info, dict):
342
424
  for key in FINAL_OBSERVATION_KEYS:
343
425
  if key in info:
344
- info[key] = self.transform_obs(info[key])
426
+ info[key] = self.transform(info[key], done = None)
345
427
 
346
428
  return obs, info
347
429
 
@@ -349,17 +431,16 @@ class TransformObservationWrapper(EnvWrapper):
349
431
  obs, reward, terminated, truncated, info = self.env.step(action)
350
432
  done = dones_of(terminated, truncated) if self.autoresets else None
351
433
 
352
- out = self.transform_obs(obs, done = done) if self.takes_done else self.transform_obs(obs)
434
+ out = self.transform(obs, done = done)
353
435
 
354
436
  if isinstance(info, dict):
355
437
  for key in FINAL_OBSERVATION_KEYS:
356
438
  if key in info:
357
439
  if not self.takes_done:
358
- info[key] = self.transform_obs(info[key])
440
+ info[key] = self.transform(info[key])
359
441
  elif not self.autoresets:
360
442
  info[key] = out
361
443
 
362
444
  return out, reward, terminated, truncated, info
363
445
 
364
446
  ObservationWrapper = TransformObservationWrapper
365
-
@@ -31,7 +31,7 @@ def render_frame(env, image_size = (64, 64), camera = None):
31
31
  img = env_render(env, height, width, camera)
32
32
 
33
33
  if not exists(img):
34
- if env_render_mode(env) is None:
34
+ if not exists(env_render_mode(env)):
35
35
  raise ValueError(
36
36
  'env must be created with render_mode = "rgb_array", '
37
37
  'e.g. gym.make(id, render_mode = "rgb_array")'
@@ -7,6 +7,8 @@ import numpy as np
7
7
  import torch
8
8
  from torch import is_tensor
9
9
 
10
+ from .helpers import default, exists
11
+
10
12
  # mock sims — stand-ins for real-world environments with varying MDP interfaces
11
13
  # all share the underlying dynamics; each emulates the quirks of a real sim
12
14
 
@@ -60,6 +62,10 @@ class DiscreteSpace:
60
62
  def sample(self):
61
63
  return np.random.randint(self.n)
62
64
 
65
+ class DictSpace:
66
+ def __init__(self, **spaces):
67
+ self.spaces = spaces
68
+
63
69
  # sim-specific render surfaces — emulating how each sim actually produces images
64
70
 
65
71
  class ActionSpec:
@@ -125,16 +131,20 @@ class MockEnv:
125
131
  return False
126
132
 
127
133
  def seed(self, seed = 0):
134
+ seed = default(seed, 0)
128
135
  self.rng = np.random.default_rng(seed)
129
136
  torch.manual_seed(seed)
130
137
  self.reset_state()
131
138
 
132
139
  def reset(self, seed = None, **kwargs):
133
- if seed is not None:
140
+ self.restart(seed)
141
+ return self.obs(), {}
142
+
143
+ def restart(self, seed = None):
144
+ if exists(seed):
134
145
  self.seed(seed)
135
146
  else:
136
147
  self.reset_state()
137
- return self.obs(), {}
138
148
 
139
149
  def reset_state(self):
140
150
  self.t = np.zeros(self.num_envs, dtype = int) if self.is_vector else 0
@@ -406,10 +416,7 @@ class MjlabMockEnv(MockEnv):
406
416
  super().__init__(seed)
407
417
 
408
418
  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()
419
+ self.restart(seed)
413
420
  return self.obs(), {}
414
421
 
415
422
  def obs(self):
@@ -487,10 +494,7 @@ class ManiSkillMockEnv(MockEnv):
487
494
  return dict(state = state) if self.obs_mode == 'state_dict' else state
488
495
 
489
496
  def reset(self, seed = None, options = None):
490
- if seed is not None:
491
- self.seed(seed)
492
- else:
493
- self.reset_state()
497
+ self.restart(seed)
494
498
  return self.obs(), dict(reconfigure = False)
495
499
 
496
500
  def step(self, action):
@@ -515,6 +519,9 @@ class JaxArray:
515
519
  def __array__(self, dtype = None):
516
520
  return np.asarray(self._arr, dtype = dtype)
517
521
 
522
+ def __getitem__(self, item):
523
+ return JaxArray(self._arr[item])
524
+
518
525
  @property
519
526
  def dtype(self):
520
527
  return self._arr.dtype
@@ -550,19 +557,15 @@ class BraxMockEnv(MockEnv):
550
557
  self.advance(action)
551
558
  return JaxArray(self.obs()), JaxArray(np.ones(self.num_envs)), JaxArray(self.is_done()), {}
552
559
 
553
- # mujoco-mjx — gymnasium-style 5-tuple step, (obs, info) reset, single env, and
554
- # every stream (obs / reward / dones) is a jax array: the dialect of envs built
555
- # on the mjx jax physics backend
560
+ # mujoco-mjx — gymnasium-style 5-tuple step, (obs, info) reset, single env,
561
+ # every stream (obs / reward / dones) a jax array
556
562
 
557
563
  class MjxMockEnv(MockEnv):
558
564
  obs_dim = 8
559
565
  action_dim = 6
560
566
 
561
567
  def reset(self, seed = None, **kwargs):
562
- if seed is not None:
563
- self.seed(seed)
564
- else:
565
- self.reset_state()
568
+ self.restart(seed)
566
569
  return JaxArray(self.obs()), {}
567
570
 
568
571
  def step(self, action):
@@ -652,3 +655,232 @@ class TupleObsMockEnv(MockEnv):
652
655
  def step(self, action):
653
656
  self.advance(action)
654
657
  return self.obs(), np.ones(self.num_envs), self.is_done(), np.zeros(self.num_envs, dtype = bool), {}
658
+
659
+ # rlbench — language-conditioned manipulation: reset -> (descriptions, obs),
660
+ # step -> (obs, reward, terminate); obs unpacks via get_low_dim_data()
661
+
662
+ class RLBenchObservation:
663
+ def __init__(self, joint_pos, gripper, wrist_rgb):
664
+ self.joint_positions = joint_pos
665
+ self.gripper_open = gripper
666
+ self.wrist_rgb = wrist_rgb
667
+
668
+ def get_low_dim_data(self):
669
+ return np.concatenate([self.joint_positions, np.array([self.gripper_open])])
670
+
671
+ class RLBenchMockEnv(MockEnv):
672
+ obs_dim = 8
673
+ action_dim = 8
674
+ max_steps = 40
675
+
676
+ def __init__(self, task_name = 'open_drawer', seed = 0):
677
+ self.task_name = task_name
678
+ self.descriptions = [f'{task_name.replace("_", " ")} task']
679
+ super().__init__(seed)
680
+
681
+ def rlbench_obs(self):
682
+ return RLBenchObservation(
683
+ joint_pos = self.obs()[:7],
684
+ gripper = 1.0,
685
+ wrist_rgb = np.zeros((64, 64, 3), dtype = np.uint8)
686
+ )
687
+
688
+ def reset(self, seed = None, **kwargs):
689
+ self.restart(seed)
690
+ return self.descriptions, self.rlbench_obs()
691
+
692
+ def step(self, action):
693
+ self.advance(action)
694
+ return self.rlbench_obs(), 1.0, self.is_done()
695
+
696
+ # omnigibson (behavior-1k) — obs-dict-only reset, 5-tuple step, sim render
697
+
698
+ class OmniGibsonMockEnv(MockEnv):
699
+ obs_dim = 14
700
+ action_dim = 6
701
+ max_steps = 100
702
+
703
+ def __init__(self, seed = 0):
704
+ super().__init__(seed)
705
+ self.sim = FakeSim()
706
+
707
+ def obs_dict(self):
708
+ return dict(
709
+ robot0_proprio = self.obs(),
710
+ rgb = np.zeros((64, 64, 3), dtype = np.uint8)
711
+ )
712
+
713
+ def reset(self, seed = None, **kwargs):
714
+ self.restart(seed)
715
+ return self.obs_dict()
716
+
717
+ def step(self, action):
718
+ self.advance(action)
719
+ return self.obs_dict(), 1.0, self.is_done(), False, {}
720
+
721
+ # robomimic — structured obs dict, obs-only reset, 4-tuple step
722
+
723
+ class RoboMimicMockEnv(MockEnv):
724
+ obs_dim = 10
725
+ action_dim = 7
726
+ max_steps = 50
727
+
728
+ def obs_dict(self):
729
+ return dict(
730
+ robot0_eef_pos = self.obs()[:3],
731
+ robot0_eef_quat = np.array([0., 0., 0., 1.]),
732
+ object = self.obs()[3:]
733
+ )
734
+
735
+ def reset(self, seed = None, **kwargs):
736
+ self.restart(seed)
737
+ return self.obs_dict()
738
+
739
+ def step(self, action):
740
+ self.advance(action)
741
+ return self.obs_dict(), 1.0, self.is_done(), {}
742
+
743
+ # myosuite — musculoskeletal mujoco robotics: high-dim muscle states,
744
+ # physics render, 5-tuple step
745
+
746
+ class MyoSuiteMockEnv(MockEnv):
747
+ obs_dim = 36
748
+ action_dim = 12
749
+ max_steps = 80
750
+
751
+ def __init__(self, seed = 0):
752
+ super().__init__(seed)
753
+ self.physics = FakePhysics()
754
+
755
+ def step(self, action):
756
+ self.advance(action)
757
+ return self.obs(), 1.0, self.is_done(), False, {}
758
+
759
+ # gymnasium-robotics (farama goal envs: fetch, shadow hand, franka kitchen) —
760
+ # dict obs (observation / achieved_goal / desired_goal), exposes compute_reward
761
+
762
+ class GymnasiumRoboticsMockEnv(MockEnv):
763
+ obs_dim = 10
764
+ goal_dim = 3
765
+ action_dim = 4
766
+ max_steps = 50
767
+
768
+ @property
769
+ def observation_space(self):
770
+ return DictSpace(
771
+ observation = Space((self.obs_dim,)),
772
+ achieved_goal = Space((self.goal_dim,)),
773
+ desired_goal = Space((self.goal_dim,)),
774
+ )
775
+
776
+ def goal_dict(self):
777
+ return dict(
778
+ observation = self.obs(),
779
+ achieved_goal = self.obs()[:self.goal_dim],
780
+ desired_goal = np.zeros(self.goal_dim)
781
+ )
782
+
783
+ def reset(self, seed = None, **kwargs):
784
+ self.restart(seed)
785
+ return self.goal_dict(), {}
786
+
787
+ def compute_reward(self, achieved_goal, desired_goal, info):
788
+ return -np.linalg.norm(achieved_goal - desired_goal, axis = -1)
789
+
790
+ def step(self, action):
791
+ self.advance(action)
792
+ obs = self.goal_dict()
793
+ reward = float(self.compute_reward(obs['achieved_goal'], obs['desired_goal'], {}))
794
+ return obs, reward, self.is_done(), False, {}
795
+
796
+ # lerobot (hugging face aloha / pusht) — bimanual multi-camera robotics:
797
+ # dict obs with visual pixels + robot state, 5-tuple step
798
+
799
+ class LeRobotMockEnv(MockEnv):
800
+ obs_dim = 14
801
+ action_dim = 14
802
+ max_steps = 60
803
+
804
+ def obs_dict(self):
805
+ return dict(
806
+ agent_pos = self.obs(),
807
+ pixels = dict(
808
+ top = np.zeros((64, 64, 3), dtype = np.uint8),
809
+ wrist = np.zeros((64, 64, 3), dtype = np.uint8)
810
+ )
811
+ )
812
+
813
+ def reset(self, seed = None, **kwargs):
814
+ self.restart(seed)
815
+ return self.obs_dict(), {}
816
+
817
+ def step(self, action):
818
+ self.advance(action)
819
+ return self.obs_dict(), 1.0, self.is_done(), False, {}
820
+
821
+ # calvin (language-conditioned multi-task 3d manipulation) — string instruction
822
+ # reset with structured vision + proprio dict, 5-tuple step
823
+
824
+ class CalvinMockEnv(MockEnv):
825
+ obs_dim = 15
826
+ action_dim = 7
827
+ max_steps = 40
828
+
829
+ def __init__(self, task_instruction = 'open drawer and push red block', seed = 0):
830
+ super().__init__(seed = seed)
831
+ self.task_instruction = task_instruction
832
+
833
+ def obs_dict(self):
834
+ return dict(
835
+ robot_obs = self.obs(),
836
+ rgb_obs = dict(
837
+ rgb_static = np.zeros((64, 64, 3), dtype = np.uint8),
838
+ rgb_gripper = np.zeros((64, 64, 3), dtype = np.uint8)
839
+ )
840
+ )
841
+
842
+ def reset(self, seed = None, **kwargs):
843
+ self.restart(seed)
844
+ return self.task_instruction, self.obs_dict()
845
+
846
+ def step(self, action):
847
+ self.advance(action)
848
+ return self.obs_dict(), 1.0, self.is_done(), False, {}
849
+
850
+ # pusht (diffusion policy planar pushing benchmark) — 2-dim eef actions in (0, 512),
851
+ # dict observation with agent_pos and block_pose
852
+
853
+ class PushTMockEnv(MockEnv):
854
+ obs_dim = 2
855
+ action_dim = 2
856
+ max_steps = 50
857
+
858
+ @property
859
+ def action_space(self):
860
+ return Space((self.action_dim,), 0., 512.)
861
+
862
+ def obs_dict(self):
863
+ return dict(
864
+ agent_pos = self.obs(),
865
+ block_pose = np.zeros(3)
866
+ )
867
+
868
+ def reset(self, seed = None, **kwargs):
869
+ self.restart(seed)
870
+ return self.obs_dict(), {}
871
+
872
+ def step(self, action):
873
+ self.advance(action)
874
+ return self.obs_dict(), 1.0, self.is_done(), False, {}
875
+
876
+ # gym-pybullet-drones / aerial robotics — 4-dim motor rpm action space [-1, 1],
877
+ # 12-dim kinematics observation (pos, rpy, vel, ang_vel)
878
+
879
+ class DroneAviaryMockEnv(MockEnv):
880
+ obs_dim = 12
881
+ action_dim = 4
882
+ max_steps = 60
883
+
884
+ def step(self, action):
885
+ self.advance(action)
886
+ return self.obs(), 1.0, self.is_done(), False, {}
@@ -43,6 +43,9 @@ def space_from_action_spec(env):
43
43
 
44
44
  # action-space duck-typing across gymnasium, dm_control, and custom envs
45
45
 
46
+ def shape_dim(shape) -> int:
47
+ return int(np.prod(shape)) if len(shape) > 0 else 1
48
+
46
49
  def action_space_dim(space) -> int | None:
47
50
  if not exists(space):
48
51
  return None
@@ -55,11 +58,30 @@ def action_space_dim(space) -> int | None:
55
58
  if exists(nvec):
56
59
  return int(np.prod(nvec))
57
60
 
61
+ shape = get_attr(space, 'shape')
62
+ return shape_dim(shape) if exists(shape) else None
63
+
64
+ def space_dim(space) -> int | None:
65
+ # flat dim of an observation space tree — composite spaces sum their children
66
+
67
+ if not exists(space):
68
+ return None
69
+
58
70
  shape = get_attr(space, 'shape')
59
71
  if exists(shape):
60
- return int(np.prod(shape)) if len(shape) > 0 else 1
72
+ return shape_dim(shape)
73
+
74
+ subspaces = get_attr(space, 'spaces')
75
+ if not exists(subspaces):
76
+ return None
77
+
78
+ child_spaces = subspaces.values() if isinstance(subspaces, dict) else subspaces
79
+ child_dims = [space_dim(child) for child in child_spaces]
80
+
81
+ if not child_dims or not all(map(exists, child_dims)):
82
+ return None
61
83
 
62
- return None
84
+ return sum(child_dims)
63
85
 
64
86
  def action_space_is_discrete(space) -> bool:
65
87
  return exists(get_attr(space, 'n'))
@@ -92,9 +114,4 @@ def action_dim_of(env) -> int | None:
92
114
  def obs_dim_of(env) -> int | None:
93
115
  from .helpers import get_adapter
94
116
  space = get_adapter(env).observation_space
95
- shape = get_attr(space, 'shape')
96
-
97
- if exists(shape):
98
- return int(np.prod(shape)) if len(shape) > 0 else 1
99
-
100
- return get_attr(env, 'obs_dim')
117
+ return default(space_dim(space), get_attr(env, 'obs_dim'))
@@ -13,7 +13,7 @@ from .helpers import (
13
13
  FINAL_OBSERVATION_MASK_KEYS,
14
14
  exists,
15
15
  get_adapter,
16
- get_attr,
16
+ is_foreign_array,
17
17
  is_scalar,
18
18
  unpack_vector_observations,
19
19
  )
@@ -26,7 +26,7 @@ def to_torch_leaf(t, device, cast_obs_to_float = True):
26
26
  t = from_numpy(t.copy())
27
27
  elif is_scalar(t):
28
28
  t = tensor(t)
29
- elif exists(get_attr(t, '__array__')):
29
+ elif is_foreign_array(t):
30
30
  t = from_numpy(np.asarray(t))
31
31
  else:
32
32
  return t
@@ -98,7 +98,7 @@ class TensorWrapper(EnvWrapper):
98
98
 
99
99
  # deprecated alias, kept for backwards compatibility
100
100
 
101
- if cast_float64_to_float32 is not None:
101
+ if exists(cast_float64_to_float32):
102
102
  cast_obs_to_float = cast_float64_to_float32
103
103
 
104
104
  self.device = torch_device(device)
@@ -144,8 +144,12 @@ class TensorWrapper(EnvWrapper):
144
144
  return tree_map(convert, t)
145
145
 
146
146
  def step(self, action):
147
- if self.convert_in and not self.torch_native:
148
- action = torch_to_numpy(action)
147
+ if self.convert_in:
148
+ if not self.torch_native:
149
+ action = torch_to_numpy(action)
150
+ elif not is_tensor(action):
151
+ # torch-native sims take torch on device — lift numpy / foreign actions
152
+ action = numpy_to_torch(action, device = self.device, cast_obs_to_float = False)
149
153
 
150
154
  obs, reward, terminated, truncated, info = self.env.step(action)
151
155
 
@@ -18,18 +18,15 @@ from .helpers import (
18
18
  )
19
19
 
20
20
  def back_to_like(t, numpy_arr):
21
- if is_tensor(t):
22
- return torch.from_numpy(numpy_arr).to(t.device)
21
+ # scalar dones stay scalar
23
22
 
24
- if isinstance(t, np.ndarray):
25
- return numpy_arr
23
+ if is_tensor(t) or isinstance(t, np.ndarray):
24
+ res = torch.from_numpy(numpy_arr).to(t.device) if is_tensor(t) else numpy_arr
25
+ return res.squeeze() if t.ndim == 0 else res
26
26
 
27
27
  # python scalars (single-env dones) collapse to a scalar; foreign
28
28
  # array-likes (jax) keep the numpy array so the batch dim survives
29
29
 
30
- if numpy_arr.ndim == 0:
31
- return bool(numpy_arr)
32
-
33
30
  if numpy_arr.size == 1:
34
31
  return bool(numpy_arr.reshape(-1)[0])
35
32
 
@@ -16,7 +16,7 @@ from .helpers import (
16
16
  truthy_attr,
17
17
  _zero_leaf,
18
18
  )
19
- from .spaces import action_dim_of
19
+ from .spaces import action_dim_of, obs_dim_of
20
20
  from .standardize_wrapper import StandardizeWrapper
21
21
 
22
22
  # leaf helpers
@@ -216,6 +216,7 @@ class MultiprocessingVecEnv:
216
216
  self.action_space = self._get_attr('action_space')
217
217
  self.observation_space = self._get_attr('observation_space')
218
218
  self.action_dim = action_dim_of(self)
219
+ self.obs_dim = obs_dim_of(self)
219
220
 
220
221
  self.seed(seed)
221
222
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "env-ssl-wrapper"
3
- version = "0.5.2"
3
+ version = "0.5.3"
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