env-ssl-wrapper 0.5.4__tar.gz → 0.5.6__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 (27) hide show
  1. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/PKG-INFO +31 -1
  2. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/README.md +30 -0
  3. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/__init__.py +14 -0
  4. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/action_chunk.py +6 -4
  5. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/evaluate.py +34 -22
  6. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/memory_trace.py +21 -2
  7. env_ssl_wrapper-0.5.6/env_ssl_wrapper/sgs.py +490 -0
  8. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/action_transform_wrapper.py +16 -17
  9. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/adapters.py +23 -95
  10. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/auto_batched_wrapper.py +4 -11
  11. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/episode_padding_wrapper.py +15 -5
  12. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/flatten_obs_wrapper.py +1 -1
  13. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/helpers.py +16 -27
  14. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/image_wrapper.py +8 -9
  15. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/spaces.py +2 -9
  16. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/standardize_env_wrapper.py +7 -1
  17. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/standardize_wrapper.py +3 -6
  18. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/tensor_wrapper.py +2 -4
  19. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/time_limit_wrapper.py +1 -1
  20. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/utils.py +5 -1
  21. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/vector.py +63 -38
  22. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/pyproject.toml +1 -1
  23. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/.gitignore +0 -0
  24. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/LICENSE +0 -0
  25. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/__init__.py +0 -0
  26. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/done_tracker_wrapper.py +0 -0
  27. {env_ssl_wrapper-0.5.4 → env_ssl_wrapper-0.5.6}/env_ssl_wrapper/standardize/mocks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: env-ssl-wrapper
3
- Version: 0.5.4
3
+ Version: 0.5.6
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
@@ -180,6 +180,26 @@ uv run test_action_chunk.py --sweep=True # PPO over several chun
180
180
  uv run test_action_chunk.py --verify=True --sweep=True # chunked rollouts match per-step reference
181
181
  ```
182
182
 
183
+ ### Success-Guided Sampling
184
+
185
+ Adaptive curriculum over a bank of reset states, keeping episodes near the policy's frontier of competence — the configurations it currently solves about `target` of the time ([Octi Zhang et al., 2026](https://arxiv.org/abs/2603.15999))
186
+
187
+ ```python
188
+ import gymnasium as gym
189
+ import torch
190
+ from env_ssl_wrapper import StandardizeEnvWrapper, SuccessGuidedSamplingWrapper
191
+
192
+ reset_states = [torch.zeros(4), torch.ones(4)] # bank of reset states to sample over
193
+
194
+ env = StandardizeEnvWrapper(gym.make('CartPole-v1'))
195
+ env = SuccessGuidedSamplingWrapper(env, reset_configs = reset_states, target = 0.5, kappa = 2.)
196
+
197
+ obs, info = env.reset()
198
+ # info['sgs_config_id'] -> the config this episode started from (success rates update on episode end)
199
+ ```
200
+
201
+ `reset_configs` holds reset states — or callables drawing one (`lambda rng: ...`) — passed through `options`, or built by a `reset_options_fn` / `state_transform`. Every stage of the sampling loop is hookable: score equation (`score_fn`), target schedule, uniform floor (`uniform_prob`), env-native fallback (`default_prob`), success signal (`success_fn`).
202
+
183
203
  ## Wrappers
184
204
 
185
205
  Pass wrappers as strings (default config) or `(name, dict)` tuples (custom config), in any order.
@@ -195,6 +215,7 @@ Pass wrappers as strings (default config) or `(name, dict)` tuples (custom confi
195
215
  | `tensor` | NumPy → torch on a device, torch actions → numpy for the sim. |
196
216
  | `flatten_obs` | Flattens dict/tuple observations into a single vector. |
197
217
  | `action_chunk` | Executes actions in open-loop chunks of length k. `('action_chunk', dict(chunk_len = 2, gamma = 0.99))` |
218
+ | `sgs` | Adaptive frontier curriculum over reset configs. `('sgs', dict(reset_configs = configs, target = 0.5, kappa = 2.))` |
198
219
 
199
220
  Every env emits the same contract: obs `torch.float32`, rewards `torch.float32`, `terminated`/`truncated` `torch.bool`. `env.seed(n)` works on every sim.
200
221
 
@@ -312,3 +333,12 @@ uv run pytest
312
333
  year = {2025}
313
334
  }
314
335
  ```
336
+
337
+ ```bibtex
338
+ @article{zhang2026omnireset,
339
+ title = {OmniReset: Solving Dexterous Manipulation from Sparse Rewards via Reset States},
340
+ author = {Octi Zhang and others},
341
+ journal = {arXiv preprint arXiv:2603.15999},
342
+ year = {2026}
343
+ }
344
+ ```
@@ -120,6 +120,26 @@ uv run test_action_chunk.py --sweep=True # PPO over several chun
120
120
  uv run test_action_chunk.py --verify=True --sweep=True # chunked rollouts match per-step reference
121
121
  ```
122
122
 
123
+ ### Success-Guided Sampling
124
+
125
+ Adaptive curriculum over a bank of reset states, keeping episodes near the policy's frontier of competence — the configurations it currently solves about `target` of the time ([Octi Zhang et al., 2026](https://arxiv.org/abs/2603.15999))
126
+
127
+ ```python
128
+ import gymnasium as gym
129
+ import torch
130
+ from env_ssl_wrapper import StandardizeEnvWrapper, SuccessGuidedSamplingWrapper
131
+
132
+ reset_states = [torch.zeros(4), torch.ones(4)] # bank of reset states to sample over
133
+
134
+ env = StandardizeEnvWrapper(gym.make('CartPole-v1'))
135
+ env = SuccessGuidedSamplingWrapper(env, reset_configs = reset_states, target = 0.5, kappa = 2.)
136
+
137
+ obs, info = env.reset()
138
+ # info['sgs_config_id'] -> the config this episode started from (success rates update on episode end)
139
+ ```
140
+
141
+ `reset_configs` holds reset states — or callables drawing one (`lambda rng: ...`) — passed through `options`, or built by a `reset_options_fn` / `state_transform`. Every stage of the sampling loop is hookable: score equation (`score_fn`), target schedule, uniform floor (`uniform_prob`), env-native fallback (`default_prob`), success signal (`success_fn`).
142
+
123
143
  ## Wrappers
124
144
 
125
145
  Pass wrappers as strings (default config) or `(name, dict)` tuples (custom config), in any order.
@@ -135,6 +155,7 @@ Pass wrappers as strings (default config) or `(name, dict)` tuples (custom confi
135
155
  | `tensor` | NumPy → torch on a device, torch actions → numpy for the sim. |
136
156
  | `flatten_obs` | Flattens dict/tuple observations into a single vector. |
137
157
  | `action_chunk` | Executes actions in open-loop chunks of length k. `('action_chunk', dict(chunk_len = 2, gamma = 0.99))` |
158
+ | `sgs` | Adaptive frontier curriculum over reset configs. `('sgs', dict(reset_configs = configs, target = 0.5, kappa = 2.))` |
138
159
 
139
160
  Every env emits the same contract: obs `torch.float32`, rewards `torch.float32`, `terminated`/`truncated` `torch.bool`. `env.seed(n)` works on every sim.
140
161
 
@@ -252,3 +273,12 @@ uv run pytest
252
273
  year = {2025}
253
274
  }
254
275
  ```
276
+
277
+ ```bibtex
278
+ @article{zhang2026omnireset,
279
+ title = {OmniReset: Solving Dexterous Manipulation from Sparse Rewards via Reset States},
280
+ author = {Octi Zhang and others},
281
+ journal = {arXiv preprint arXiv:2603.15999},
282
+ year = {2026}
283
+ }
284
+ ```
@@ -7,6 +7,14 @@ from .standardize import * # noqa: F403
7
7
 
8
8
  from .memory_trace import MemoryTraceWrapper
9
9
  from .action_chunk import ActionChunkWrapper
10
+ from .sgs import (
11
+ SuccessGuidedSamplingWrapper,
12
+ SGSWrapper,
13
+ SGSSampler,
14
+ sgs_score_fn,
15
+ variance_score_fn,
16
+ uniform_score_fn,
17
+ )
10
18
  from .evaluate import evaluate_actor, EpisodeStats
11
19
 
12
20
  # backwards compatibility — the standardize wrappers used to live at the top
@@ -38,6 +46,12 @@ __all__ = [
38
46
  *standardize.__all__,
39
47
  'MemoryTraceWrapper',
40
48
  'ActionChunkWrapper',
49
+ 'SuccessGuidedSamplingWrapper',
50
+ 'SGSWrapper',
51
+ 'SGSSampler',
52
+ 'sgs_score_fn',
53
+ 'variance_score_fn',
54
+ 'uniform_score_fn',
41
55
  'evaluate_actor',
42
56
  'EpisodeStats',
43
57
  ]
@@ -7,15 +7,17 @@ from torch import is_tensor
7
7
 
8
8
  from .standardize.helpers import (
9
9
  EnvWrapper,
10
+ any_true,
10
11
  copy_leaf,
11
12
  default,
12
- any_true,
13
13
  dones_of,
14
14
  env_takes_torch,
15
15
  first_existing,
16
16
  get_adapter,
17
17
  get_attr,
18
18
  is_vectorized,
19
+ normalize_reset_out,
20
+ normalize_step_out,
19
21
  to_numpy,
20
22
  )
21
23
 
@@ -72,7 +74,7 @@ class ActionChunkWrapper(EnvWrapper):
72
74
 
73
75
  # vectorized gym envs expose a batched action space - the single env space defines the chunk layout
74
76
 
75
- action_space = first_existing(env, 'single_action_space', 'action_space')
77
+ action_space = default(first_existing(env, 'single_action_space', 'action_space'), get_adapter(env).action_space)
76
78
  self.action_shape = tuple(get_attr(action_space, 'shape', ()) or ())
77
79
 
78
80
  self.expects_batch = is_vectorized(env)
@@ -83,7 +85,7 @@ class ActionChunkWrapper(EnvWrapper):
83
85
  return (self.chunk_len, *self.action_shape)
84
86
 
85
87
  def reset(self, **kwargs):
86
- return self.env.reset(**kwargs)
88
+ return normalize_reset_out(self.env.reset(**kwargs))
87
89
 
88
90
  def step(self, actions):
89
91
  if isinstance(actions, dict):
@@ -114,7 +116,7 @@ class ActionChunkWrapper(EnvWrapper):
114
116
  if not self.expects_batch:
115
117
  action = unbatch_step(action)
116
118
 
117
- out = self.env.step(action)
119
+ out = normalize_step_out(self.env.step(action))
118
120
  rewards.append(copy_leaf(out[1]))
119
121
 
120
122
  if any_true(dones_of(out[2], out[3])):
@@ -6,12 +6,11 @@ from typing import NamedTuple
6
6
  import numpy as np
7
7
  import torch
8
8
  from torch import nn, is_tensor
9
- from torch.distributions import Categorical, Distribution
10
- from torch.utils._pytree import tree_flatten, tree_map
9
+ from torch.distributions import Bernoulli, Categorical, Distribution
10
+ from torch.utils._pytree import tree_map
11
11
 
12
12
  from .standardize.spaces import action_space_is_discrete
13
13
  from .standardize.helpers import (
14
- EnvWrapper,
15
14
  dones_of,
16
15
  env_autoresets,
17
16
  env_num_envs,
@@ -97,6 +96,12 @@ def to_action(env, out, deterministic = True):
97
96
  if not deterministic:
98
97
  return val.sample()
99
98
 
99
+ if isinstance(val, Bernoulli):
100
+ return (val.probs > 0.5).long()
101
+
102
+ if isinstance(val, Categorical):
103
+ return val.probs.argmax(dim = -1)
104
+
100
105
  probs = get_attr(val, 'probs')
101
106
 
102
107
  # categorical-style probs carry a trailing action axis to argmax over
@@ -171,24 +176,26 @@ def to_env_action(env, action):
171
176
  if not env_takes_torch(env):
172
177
  action = tree_map(to_numpy, action)
173
178
 
174
- # single raw sims get the leading batch dim stripped, while discrete
175
- # actions additionally collapse to scalars
179
+ # single non-vectorized envs get the leading batch dim stripped, while discrete
180
+ # actions additionally collapse to scalars
176
181
 
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]
182
+ if not is_vectorized(env) and not exists(chunk_shape):
183
+ is_discrete = action_space_is_discrete(get_adapter(env).action_space)
181
184
 
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
+ def strip_leaf(a):
186
+ if a.ndim > 1 and a.shape[0] == 1:
187
+ a = a[0]
185
188
 
186
- if a.ndim == 0:
187
- a = a.item()
189
+ if is_discrete:
190
+ if a.ndim > 0 and a.shape[0] == 1:
191
+ a = a[0]
192
+
193
+ if a.ndim == 0:
194
+ a = a.item()
188
195
 
189
- return a
196
+ return a
190
197
 
191
- action = tree_map(strip_leaf, action)
198
+ action = tree_map(strip_leaf, action)
192
199
 
193
200
  return action
194
201
 
@@ -292,7 +299,7 @@ def evaluate_actor(
292
299
  assert not (record and num_envs > 1), 'video recording requires a single env'
293
300
 
294
301
  obs, info = safe_reset(env, seed)
295
- batched = is_vectorized(env) or all(map(is_tensor, tree_flatten(obs)[0]))
302
+ batched = is_vectorized(env)
296
303
 
297
304
  goal = goal.expand(num_envs, -1) if exists(goal) and goal.shape[0] == 1 and num_envs > 1 else goal
298
305
 
@@ -326,10 +333,12 @@ def evaluate_actor(
326
333
  if reward.numel() > num_envs:
327
334
  reward = reward.reshape(num_envs, -1).sum(dim = -1)
328
335
 
329
- returns += reward if reward.numel() == num_envs else reward.expand(num_envs)
336
+ reward = reward if reward.numel() == num_envs else reward.expand(num_envs)
337
+ active_t = torch.from_numpy(active)
330
338
 
331
- lengths += info.get('chunk_length', 1) if isinstance(info, dict) else 1
332
- steps += 1
339
+ returns += torch.where(active_t, reward, 0.)
340
+ lengths += torch.where(active_t, float(info.get('chunk_length', 1) if isinstance(info, dict) else 1), 0.)
341
+ steps += torch.where(active_t, 1, 0)
333
342
 
334
343
  done = to_numpy(dones_of(terminated, truncated)).astype(bool).reshape(-1)
335
344
 
@@ -367,15 +376,18 @@ def evaluate_actor(
367
376
  steps[i] = 0
368
377
  active[i] = False
369
378
 
370
- if done.any() and len(done_returns) < episodes:
379
+ if (done.any() or not active.any()) and len(done_returns) < episodes:
371
380
  if record:
372
381
  frames = []
373
382
 
374
383
  # autoresetting envs reset themselves, everything else resets here
375
384
 
376
- if not env_autoresets(env) and (num_envs == 1 or done.all() or bool(get_attr(env, 'needs_reset', False))):
385
+ if not env_autoresets(env) and (num_envs == 1 or not active.any() or done.all() or bool(get_attr(env, 'needs_reset', False))):
377
386
  reset_seed = seed + len(done_returns) if exists(seed) and num_envs == 1 else None
378
387
  obs, info = safe_reset(env, reset_seed)
388
+ returns.zero_()
389
+ lengths.zero_()
390
+ steps.zero_()
379
391
  active[:] = True
380
392
  elif env_autoresets(env):
381
393
  active[newly_done] = True
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ from functools import partial
2
3
  from typing import Sequence
3
4
 
4
5
  import torch
@@ -9,9 +10,12 @@ from .standardize.helpers import (
9
10
  any_true,
10
11
  cast_tuple,
11
12
  default,
13
+ dones_of,
12
14
  exists,
13
15
  is_array_like,
14
16
  is_scalar,
17
+ maybe_transform_final_observation,
18
+ normalize_step_out,
15
19
  )
16
20
 
17
21
  # helper functions
@@ -63,7 +67,7 @@ class MemoryTraceWrapper(TransformObservationWrapper):
63
67
  prefix = self.trace_key if key == self.obs_key else f'{key}_{self.trace_key}'
64
68
  return f'{prefix}_{lam}' if len(self.lambdas) > 1 else prefix
65
69
 
66
- def transform_obs(self, obs, done = None):
70
+ def transform_obs(self, obs, done = None, update_state: bool = True):
67
71
  if isinstance(obs, (tuple, list)):
68
72
  out = {f'{self.obs_key}_{i}': elem for i, elem in enumerate(obs)}
69
73
  elif isinstance(obs, dict):
@@ -96,7 +100,8 @@ class MemoryTraceWrapper(TransformObservationWrapper):
96
100
  else:
97
101
  traces = [prev.lerp(val_float, calc_lerp_weight(lam, done, val_float)) for lam, prev in zip(self.lambdas, self.traces[key])]
98
102
 
99
- self.traces[key] = traces
103
+ if update_state:
104
+ self.traces[key] = traces
100
105
 
101
106
  for lam, trace in zip(self.lambdas, traces):
102
107
  out[self.trace_key_for(key, lam)] = trace
@@ -106,3 +111,17 @@ class MemoryTraceWrapper(TransformObservationWrapper):
106
111
  def reset(self, **kwargs):
107
112
  self.traces = dict()
108
113
  return super().reset(**kwargs)
114
+
115
+ def step(self, action):
116
+ obs, reward, terminated, truncated, info = normalize_step_out(self.env.step(action))
117
+ done = dones_of(terminated, truncated)
118
+
119
+ # terminal observations are transformed against pre-reset traces without mutating state
120
+
121
+ maybe_transform_final_observation(
122
+ info,
123
+ partial(self.transform_obs, done = None, update_state = False)
124
+ )
125
+
126
+ out = self.transform_obs(obs, done = done, update_state = True)
127
+ return out, reward, terminated, truncated, info