tau-ctrl 0.1.0__tar.gz → 0.2.0__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 (32) hide show
  1. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/PKG-INFO +7 -9
  2. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/README.md +5 -7
  3. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/pyproject.toml +2 -2
  4. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/__init__.py +13 -5
  5. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/__init__.py +5 -4
  6. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/base.py +81 -11
  7. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/ilqr.py +17 -5
  8. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/mppi.py +7 -1
  9. tau_ctrl-0.2.0/src/tau_ctrl/algorithms/mujoco.py +72 -0
  10. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/ppo.py +12 -6
  11. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/sac.py +9 -3
  12. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/strategy.py +9 -9
  13. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/td3.py +9 -3
  14. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/vec_env.py +4 -4
  15. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl.egg-info/PKG-INFO +7 -9
  16. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl.egg-info/SOURCES.txt +1 -0
  17. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/LICENSE +0 -0
  18. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/setup.cfg +0 -0
  19. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/adapters.py +0 -0
  20. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/cbf.py +0 -0
  21. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/envs/__init__.py +0 -0
  22. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/envs/toy.py +0 -0
  23. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/off_policy.py +0 -0
  24. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/algorithms/pid.py +0 -0
  25. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/py.typed +0 -0
  26. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/tuning/__init__.py +0 -0
  27. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/tuning/auto_tuner.py +0 -0
  28. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/tuning/bayesian_opt.py +0 -0
  29. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl/tuning/genetic_opt.py +0 -0
  30. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl.egg-info/dependency_links.txt +0 -0
  31. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl.egg-info/requires.txt +0 -0
  32. {tau_ctrl-0.1.0 → tau_ctrl-0.2.0}/src/tau_ctrl.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tau-ctrl
3
- Version: 0.1.0
4
- Summary: SB3-like, simulator-agnostic control algorithms over any gymnasium.Env — PID, MPPI/CEM/iCEM, iLQR, CBF, and GPU-native RL (PPO/SAC/TD3) with vectorized on-device training
3
+ Version: 0.2.0
4
+ Summary: Simulator-agnostic control algorithms over any gymnasium.Env — PID, MPPI/CEM/iCEM, iLQR, CBF, and GPU-native RL (PPO/SAC/TD3) with vectorized on-device training
5
5
  Author: Tau Intelligence
6
6
  License: Apache-2.0
7
7
  Project-URL: Homepage, https://tau-intelligence.com/
@@ -40,9 +40,9 @@ Dynamic: license-file
40
40
 
41
41
  # tau-ctrl
42
42
 
43
- **SB3-like, simulator-agnostic control algorithms — feedback, sampling-based MPC, safety filtering, and GPU-native RL behind one interface.**
43
+ **Simulator-agnostic control algorithms — feedback, sampling-based MPC, safety filtering, and GPU-native RL behind one interface.**
44
44
 
45
- Like Stable-Baselines3, but for the whole controller spectrum, over any `gymnasium.Env`. No simulator dependency: works with whatever hands you an env (e.g. [tau-sim](https://tau-intelligence.com/tau_sim/)). Unlike SB3, the RL methods train **on-device** and scale to vectorized environments for real GPU speedup.
45
+ A unified library for the whole controller spectrum, over any `gymnasium.Env`. No simulator dependency: works with whatever hands you an env (e.g. [tau-sim](https://tau-intelligence.com/tau_sim/)). The RL methods train **on-device** and scale to vectorized environments for real GPU speedup.
46
46
 
47
47
  ## Installation
48
48
 
@@ -57,7 +57,7 @@ pip install tau-ctrl[torch] # + RL: PPO, SAC, TD3, and vectorized on-device
57
57
  from tau_ctrl import make
58
58
 
59
59
  ctrl = make("mppi", env, horizon=25, n_samples=300) # or MPPI(env, ...), SAC(env), ...
60
- action, _ = ctrl.predict(obs) # SB3-style
60
+ action, _ = ctrl.predict(obs) # standard interface
61
61
  ctrl.learn(total_timesteps=100_000) # trainable methods (ppo/sac/td3)
62
62
  ctrl.save("ctrl.pkl")
63
63
  ```
@@ -84,7 +84,7 @@ seeding of torch's RNG).
84
84
 
85
85
  ## GPU-native RL: vectorized, on-device training
86
86
 
87
- SB3 steps CPU environments and the policy update sits behind per-step Python.
87
+ Standard pipelines step CPU environments sequentially and perform updates in slow Python loops.
88
88
  tau-ctrl's SAC/TD3 instead run the replay buffer and the update **on the target
89
89
  device**, and — given a batched env — step thousands of environments in parallel
90
90
  with no numpy in the hot loop. `Trainer.auto` probes your env and hardware and
@@ -108,8 +108,7 @@ action, _ = model.predict(obs)
108
108
  | a single, non-batchable env (PyBullet, classic MuJoCo) + a factory | `SyncTorchVecEnv` | batched update on device |
109
109
  | one env you can't replicate (a real robot) | — (single-env path) | only the update |
110
110
 
111
- See [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md) for head-to-head numbers vs
112
- Stable-Baselines3 and skrl, and [`examples/`](examples/) for runnable scripts
111
+ See [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md) for performance comparison benchmarks, and [`examples/`](examples/) for runnable scripts
113
112
  (`quickstart.py`, `vectorized_rl.py`, `adaptive_training.py`).
114
113
 
115
114
  ## Safety filtering
@@ -151,4 +150,3 @@ Apache 2.0 — see [LICENSE](LICENSE).
151
150
  ## Related
152
151
 
153
152
  - [tau-sim](https://tau-intelligence.com/tau_sim/) — robotics environment builder
154
- - [Stable-Baselines3](https://stable-baselines3.readthedocs.io/) — RL algorithms (API inspiration)
@@ -1,8 +1,8 @@
1
1
  # tau-ctrl
2
2
 
3
- **SB3-like, simulator-agnostic control algorithms — feedback, sampling-based MPC, safety filtering, and GPU-native RL behind one interface.**
3
+ **Simulator-agnostic control algorithms — feedback, sampling-based MPC, safety filtering, and GPU-native RL behind one interface.**
4
4
 
5
- Like Stable-Baselines3, but for the whole controller spectrum, over any `gymnasium.Env`. No simulator dependency: works with whatever hands you an env (e.g. [tau-sim](https://tau-intelligence.com/tau_sim/)). Unlike SB3, the RL methods train **on-device** and scale to vectorized environments for real GPU speedup.
5
+ A unified library for the whole controller spectrum, over any `gymnasium.Env`. No simulator dependency: works with whatever hands you an env (e.g. [tau-sim](https://tau-intelligence.com/tau_sim/)). The RL methods train **on-device** and scale to vectorized environments for real GPU speedup.
6
6
 
7
7
  ## Installation
8
8
 
@@ -17,7 +17,7 @@ pip install tau-ctrl[torch] # + RL: PPO, SAC, TD3, and vectorized on-device
17
17
  from tau_ctrl import make
18
18
 
19
19
  ctrl = make("mppi", env, horizon=25, n_samples=300) # or MPPI(env, ...), SAC(env), ...
20
- action, _ = ctrl.predict(obs) # SB3-style
20
+ action, _ = ctrl.predict(obs) # standard interface
21
21
  ctrl.learn(total_timesteps=100_000) # trainable methods (ppo/sac/td3)
22
22
  ctrl.save("ctrl.pkl")
23
23
  ```
@@ -44,7 +44,7 @@ seeding of torch's RNG).
44
44
 
45
45
  ## GPU-native RL: vectorized, on-device training
46
46
 
47
- SB3 steps CPU environments and the policy update sits behind per-step Python.
47
+ Standard pipelines step CPU environments sequentially and perform updates in slow Python loops.
48
48
  tau-ctrl's SAC/TD3 instead run the replay buffer and the update **on the target
49
49
  device**, and — given a batched env — step thousands of environments in parallel
50
50
  with no numpy in the hot loop. `Trainer.auto` probes your env and hardware and
@@ -68,8 +68,7 @@ action, _ = model.predict(obs)
68
68
  | a single, non-batchable env (PyBullet, classic MuJoCo) + a factory | `SyncTorchVecEnv` | batched update on device |
69
69
  | one env you can't replicate (a real robot) | — (single-env path) | only the update |
70
70
 
71
- See [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md) for head-to-head numbers vs
72
- Stable-Baselines3 and skrl, and [`examples/`](examples/) for runnable scripts
71
+ See [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md) for performance comparison benchmarks, and [`examples/`](examples/) for runnable scripts
73
72
  (`quickstart.py`, `vectorized_rl.py`, `adaptive_training.py`).
74
73
 
75
74
  ## Safety filtering
@@ -111,4 +110,3 @@ Apache 2.0 — see [LICENSE](LICENSE).
111
110
  ## Related
112
111
 
113
112
  - [tau-sim](https://tau-intelligence.com/tau_sim/) — robotics environment builder
114
- - [Stable-Baselines3](https://stable-baselines3.readthedocs.io/) — RL algorithms (API inspiration)
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tau-ctrl"
7
- version = "0.1.0"
8
- description = "SB3-like, simulator-agnostic control algorithms over any gymnasium.Env — PID, MPPI/CEM/iCEM, iLQR, CBF, and GPU-native RL (PPO/SAC/TD3) with vectorized on-device training"
7
+ version = "0.2.0"
8
+ description = "Simulator-agnostic control algorithms over any gymnasium.Env — PID, MPPI/CEM/iCEM, iLQR, CBF, and GPU-native RL (PPO/SAC/TD3) with vectorized on-device training"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
11
11
  requires-python = ">=3.9"
@@ -1,12 +1,12 @@
1
1
  """
2
- tau-ctrl — an SB3-like, simulator-agnostic controller/algorithm framework.
2
+ tau-ctrl — a simulator-agnostic controller/algorithm framework.
3
3
 
4
- Call algorithms directly off the package, the way you would with SB3:
4
+ Call algorithms directly off the package:
5
5
 
6
6
  from tau_ctrl import make, PID, MPPI, CEM, ICEM, ILQR, CBFFilter, PPO, SAC, TD3
7
7
 
8
8
  ctrl = make("mppi", env, horizon=25, n_samples=200) # or PID(env), PPO(env), ...
9
- action, _ = ctrl.predict(obs) # SB3-style
9
+ action, _ = ctrl.predict(obs) # returns action, next_state
10
10
  ctrl.learn(total_timesteps=100_000) # trainable methods (ppo/sac/td3)
11
11
 
12
12
  Every algorithm shares one interface (``predict``/``learn``/``save``/``load``)
@@ -20,10 +20,15 @@ Model-based methods (MPPI/CEM/ICEM/ILQR/CBF) need the env to be *branchable*
20
20
  env. :mod:`tau_ctrl.tuning` adds Bayesian/genetic auto-tuning of any gains.
21
21
  """
22
22
 
23
- __version__ = "0.1.0"
23
+ from importlib.metadata import PackageNotFoundError
24
+ from importlib.metadata import version as _pkg_version
25
+
26
+ try:
27
+ __version__ = _pkg_version("tau-ctrl")
28
+ except PackageNotFoundError: # not installed (e.g. running from a source checkout)
29
+ __version__ = "0.0.0+unknown"
24
30
 
25
31
  from .algorithms import (
26
- CBFFilter,
27
32
  CEM,
28
33
  ICEM,
29
34
  ILQR,
@@ -33,7 +38,9 @@ from .algorithms import (
33
38
  SAC,
34
39
  TD3,
35
40
  BaseController,
41
+ CBFFilter,
36
42
  ControllerManifest,
43
+ MujocoBranchable,
37
44
  Trainer,
38
45
  available,
39
46
  get_state,
@@ -63,6 +70,7 @@ __all__ = [
63
70
  "ICEM",
64
71
  "ILQR",
65
72
  "CBFFilter",
73
+ "MujocoBranchable",
66
74
  "PPO",
67
75
  "SAC",
68
76
  "TD3",
@@ -1,12 +1,11 @@
1
- """tau-ctrl's SB3-like, simulator-agnostic controller/algorithm framework.
1
+ """tau-ctrl's simulator-agnostic controller/algorithm framework.
2
2
 
3
- One interface for the whole controller spectrum over a Gymnasium env, called
4
- the way you'd call SB3:
3
+ One interface for the whole controller spectrum over a Gymnasium env:
5
4
 
6
5
  from tau_ctrl import make, PID, MPPI, CEM, ICEM, ILQR, CBFFilter, PPO, SAC, TD3
7
6
 
8
7
  ctrl = make("mppi", env, horizon=25, n_samples=200) # or PID(env), PPO(env), ...
9
- action, _ = ctrl.predict(obs) # SB3-style
8
+ action, _ = ctrl.predict(obs) # returns action, next_state
10
9
  ctrl.learn(total_timesteps=100_000) # for trainable ones (ppo/sac/td3)
11
10
 
12
11
  Design goals:
@@ -38,6 +37,7 @@ from .base import (
38
37
  from .cbf import CBFFilter
39
38
  from .ilqr import ILQR
40
39
  from .mppi import CEM, ICEM, MPPI
40
+ from .mujoco import MujocoBranchable
41
41
  from .pid import PID
42
42
  from .ppo import PPO
43
43
  from .sac import SAC
@@ -72,6 +72,7 @@ __all__ = [
72
72
  "ICEM",
73
73
  "ILQR",
74
74
  "CBFFilter",
75
+ "MujocoBranchable",
75
76
  "PPO",
76
77
  "SAC",
77
78
  "TD3",
@@ -1,9 +1,9 @@
1
- """Core abstractions for tau-ctrl's algorithm framework — an SB3-like,
2
- simulator-agnostic controller framework over a Gymnasium environment.
1
+ """Core abstractions for tau-ctrl's algorithm framework — a simulator-agnostic
2
+ controller framework over a Gymnasium environment.
3
3
 
4
4
  Every method (PID, MPPI, CEM-MPC, CBF safety filter, PPO, ...) implements the
5
- same :class:`BaseController` interface — ``predict(obs) -> action`` like SB3,
6
- plus an optional ``learn()`` for the ones that train. Nothing here imports a
5
+ same :class:`BaseController` interface — ``predict(obs) -> action``, plus
6
+ an optional ``learn()`` for the ones that train. Nothing here imports a
7
7
  simulator: controllers talk only to the ``gymnasium.Env`` API. Methods that
8
8
  need to roll the dynamics forward (MPPI, CEM, CBF) do so through the optional
9
9
  *branching* capability (:func:`get_state` / :func:`set_state`), which any env
@@ -37,16 +37,78 @@ def register(name: str):
37
37
 
38
38
 
39
39
  def make(name: str, env: Any, **kwargs: Any) -> "BaseController":
40
- """Construct a controller by name, SB3-style: ``make("mppi", env, ...)``."""
40
+ """Construct a controller by name: ``make("mppi", env, ...)``."""
41
41
  if name not in _REGISTRY:
42
42
  raise KeyError(f"unknown controller {name!r}; available: {sorted(_REGISTRY)}")
43
+ _reject_raw_vector_env(env)
43
44
  return _REGISTRY[name](env, **kwargs)
44
45
 
45
46
 
47
+ def _reject_raw_vector_env(env: Any) -> None:
48
+ """Guard against handing a raw ``gymnasium.vector.VectorEnv`` to a controller.
49
+
50
+ Its *batched* spaces would size the networks wrong (``obs_dim`` would come
51
+ out as ``num_envs * obs_dim``) and its ``reset``/``step`` signature doesn't
52
+ match the on-device vectorized trainer, so ``learn()`` would crash. Route
53
+ vectorized envs through :meth:`Trainer.auto`, which wraps them
54
+ (``GymVectorAdapter``) and picks a convergence-safe update ratio. A native
55
+ ``tau_ctrl`` ``TorchVecEnv`` (incl. the adapters) is fine and passes through.
56
+ """
57
+ try:
58
+ import gymnasium # noqa: PLC0415
59
+
60
+ is_gym_vec = isinstance(env, gymnasium.vector.VectorEnv)
61
+ except Exception:
62
+ is_gym_vec = False
63
+ if is_gym_vec:
64
+ raise TypeError(
65
+ "make() received a gymnasium.vector.VectorEnv, which cannot be used "
66
+ "directly (its batched spaces mis-size the networks). Train vectorized "
67
+ "envs via Trainer.auto, which adapts them to the on-device loop:\n"
68
+ " from tau_ctrl import Trainer\n"
69
+ " model = Trainer.auto('sac', env=vec_env, total_timesteps=...)"
70
+ )
71
+
72
+
46
73
  def available() -> list[str]:
47
74
  return sorted(_REGISTRY)
48
75
 
49
76
 
77
+ def require_torch():
78
+ """Import and return torch, or raise a clear, actionable error.
79
+
80
+ The RL methods (PPO/SAC/TD3) and ``Trainer.auto`` need PyTorch, which ships
81
+ only via the optional extra. Without this, a user hits a bare
82
+ ``ModuleNotFoundError: No module named 'torch'`` with no hint of the fix.
83
+ """
84
+ try:
85
+ import torch # noqa: PLC0415
86
+ except ImportError as e:
87
+ raise ImportError(
88
+ "PyTorch is required for the RL methods (PPO/SAC/TD3) and Trainer.auto, "
89
+ "but it is not installed. Install it with:\n"
90
+ " pip install tau-ctrl[torch]"
91
+ ) from e
92
+ return torch
93
+
94
+
95
+ def require_box_action_space(action_space: Any, algo: str) -> None:
96
+ """Reject non-continuous action spaces up front with a clear message.
97
+
98
+ PID and the RL/MPC controllers all assume a continuous ``Box`` action
99
+ space; handing them a ``Discrete`` env otherwise fails deep inside with an
100
+ opaque ``AttributeError: 'Discrete' object has no attribute 'low'``.
101
+ """
102
+ from gymnasium import spaces # noqa: PLC0415
103
+
104
+ if not isinstance(action_space, spaces.Box):
105
+ raise TypeError(
106
+ f"{algo} requires a continuous Box action space, got "
107
+ f"{type(action_space).__name__}. These controllers are for continuous "
108
+ "control; discrete-action envs are not supported."
109
+ )
110
+
111
+
50
112
  # ---------------------------------------------------------------------------
51
113
  # Manifest
52
114
  # ---------------------------------------------------------------------------
@@ -73,23 +135,31 @@ class BranchingNotSupported(RuntimeError):
73
135
  def get_state(env: Any) -> Any:
74
136
  """Snapshot the env's dynamical state so a rollout can branch from it.
75
137
 
76
- Resolution order (no simulator assumptions): an explicit ``get_state`` on
77
- the (unwrapped) env, else classic-control ``.state``. Simulators that want
78
- model-based control just expose ``get_state``/``set_state``.
138
+ Resolution order (no simulator assumptions): an explicit ``get_state``
139
+ anywhere on the wrapper stack — the env itself or a branchable wrapper such
140
+ as :class:`~tau_ctrl.algorithms.mujoco.MujocoBranchable` layered over a sim
141
+ that doesn't expose state natively — else classic-control ``.state``.
142
+ Simulators that want model-based control just expose ``get_state``/``set_state``.
79
143
  """
80
144
  u = getattr(env, "unwrapped", env)
145
+ if hasattr(env, "get_state"):
146
+ return env.get_state()
81
147
  if hasattr(u, "get_state"):
82
148
  return u.get_state()
83
149
  if hasattr(u, "state") and u.state is not None:
84
150
  return np.array(u.state, dtype=float)
85
151
  raise BranchingNotSupported(
86
152
  f"{type(u).__name__} exposes no get_state()/.state; model-based methods "
87
- "(MPPI/CEM/CBF) need a branchable env."
153
+ "(MPPI/CEM/CBF) need a branchable env. For MuJoCo envs, wrap with "
154
+ "tau_ctrl.MujocoBranchable(env)."
88
155
  )
89
156
 
90
157
 
91
158
  def set_state(env: Any, state: Any) -> None:
92
159
  u = getattr(env, "unwrapped", env)
160
+ if hasattr(env, "set_state"):
161
+ env.set_state(state)
162
+ return
93
163
  if hasattr(u, "set_state"):
94
164
  u.set_state(state)
95
165
  return
@@ -131,7 +201,7 @@ def resolve_device(pref: str = "auto") -> str:
131
201
  # ---------------------------------------------------------------------------
132
202
 
133
203
  class BaseController(ABC):
134
- """SB3-like controller: ``predict`` always; ``learn`` when trainable."""
204
+ """Base controller: ``predict`` always; ``learn`` when trainable."""
135
205
 
136
206
  name: str = "base"
137
207
  manifest: ControllerManifest
@@ -164,7 +234,7 @@ class BaseController(ABC):
164
234
  def predict(
165
235
  self, obs: Any, state: Any = None, deterministic: bool = True
166
236
  ) -> tuple[np.ndarray, Any]:
167
- """Return ``(action, next_internal_state)`` — SB3 signature."""
237
+ """Return ``(action, next_internal_state)``."""
168
238
 
169
239
  def learn(self, total_timesteps: int = 0, **kwargs: Any) -> "BaseController":
170
240
  """Train the controller. Analytic controllers are no-ops."""
@@ -56,6 +56,11 @@ class ILQR(BaseController):
56
56
  state0 = get_state(self.env)
57
57
  self.nx = int(np.asarray(state0).size)
58
58
  self._u_nom = np.zeros((self.horizon, self.nu))
59
+ # Probe/roll out against the *unwrapped* env (see MPPI): stepping the
60
+ # wrapped env would trip TimeLimit truncation mid-rollout (producing a
61
+ # short trajectory and an IndexError in the backward pass) and corrupt
62
+ # the live episode's wrapper state.
63
+ self._rollout_env = getattr(self.env, "unwrapped", self.env)
59
64
 
60
65
  def reset(self) -> None:
61
66
  self._u_nom[:] = 0.0
@@ -67,7 +72,7 @@ class ILQR(BaseController):
67
72
  """One step from (x, u): returns (next_state, reward)."""
68
73
  set_state(self.env, x)
69
74
  u = self._clip_action(u)
70
- _, r, _, _, _ = self.env.step(u)
75
+ _, r, _, _, _ = self._rollout_env.step(u)
71
76
  return np.asarray(get_state(self.env), dtype=float), float(r)
72
77
 
73
78
  def _linearize(self, x: np.ndarray, u: np.ndarray):
@@ -112,7 +117,7 @@ class ILQR(BaseController):
112
117
  for t in range(self.horizon):
113
118
  set_state(self.env, x)
114
119
  u = self._clip_action(u_seq[t])
115
- _, r, term, trunc, _ = self.env.step(u)
120
+ _, r, term, trunc, _ = self._rollout_env.step(u)
116
121
  x = np.asarray(get_state(self.env), dtype=float)
117
122
  xs.append(x.copy())
118
123
  total_cost += -float(r)
@@ -154,14 +159,21 @@ class ILQR(BaseController):
154
159
  xs, best_cost = self._rollout(state0, u_seq)
155
160
 
156
161
  for _ in range(self.iterations):
157
- K, k = self._backward_pass(xs[: len(u_seq)], u_seq)
162
+ # A rollout can terminate before the full horizon (a genuinely
163
+ # terminating env, e.g. the ant falling), leaving fewer states than
164
+ # controls. Only optimize over the segment we actually have a
165
+ # trajectory for; controls past it stay at their nominal value.
166
+ H = min(len(u_seq), len(xs) - 1)
167
+ if H <= 0:
168
+ break
169
+ K, k = self._backward_pass(xs[:H], u_seq[:H])
158
170
  improved = False
159
171
  for alpha in self.line_search_steps:
160
172
  if alpha == 0.0:
161
173
  break # no-op fallback: keep current nominal
162
- u_try = np.zeros_like(u_seq)
174
+ u_try = u_seq.copy()
163
175
  x = state0.copy()
164
- for t in range(len(u_seq)):
176
+ for t in range(H):
165
177
  u_try[t] = self._clip_action(
166
178
  u_seq[t] + alpha * k[t] + K[t] @ (x - xs[t])
167
179
  )
@@ -56,6 +56,12 @@ class _SamplingMPC(BaseController):
56
56
  def _setup(self) -> None:
57
57
  self.nu = self.action_dim
58
58
  self._nominal = np.zeros((self.horizon, self.nu))
59
+ # Roll candidate sequences out against the *unwrapped* env. get_state /
60
+ # set_state already operate on env.unwrapped, so stepping the wrapped env
61
+ # here would leak into stateful wrappers (TimeLimit._elapsed_steps,
62
+ # OrderEnforcing) and corrupt/terminate the live episode — while the
63
+ # dynamics and reward are identical either way.
64
+ self._rollout_env = getattr(self.env, "unwrapped", self.env)
59
65
  self._venv = None
60
66
  if self._vector_env_fn is not None:
61
67
  self._venv = self._vector_env_fn(self.n_samples)
@@ -101,7 +107,7 @@ class _SamplingMPC(BaseController):
101
107
  set_state(self.env, state0)
102
108
  disc = 1.0
103
109
  for h in range(self.horizon):
104
- _, r, term, trunc, _ = self.env.step(seqs[k, h])
110
+ _, r, term, trunc, _ = self._rollout_env.step(seqs[k, h])
105
111
  R[k] += disc * r
106
112
  disc *= self.gamma
107
113
  if term or trunc:
@@ -0,0 +1,72 @@
1
+ """Make a MuJoCo Gymnasium env *branchable* for the model-based controllers.
2
+
3
+ The sampling/gradient planners (MPPI/CEM/ICEM/ILQR) and the CBF safety filter
4
+ plan by rolling candidate action sequences forward from the current state, which
5
+ needs the env to expose ``get_state``/``set_state`` (see
6
+ :mod:`tau_ctrl.algorithms.base`). Classic-control envs provide this via
7
+ ``.state``; MuJoCo envs don't. :class:`MujocoBranchable` adds it by
8
+ snapshotting/restoring MuJoCo's *full physics state* (time, ``qpos``, ``qvel``,
9
+ actuator activation, plugin state) through ``mj_getState``/``mj_setState`` — the
10
+ complete, deterministic state, not just ``qpos``/``qvel``::
11
+
12
+ import gymnasium as gym
13
+ from tau_ctrl import make, MujocoBranchable
14
+
15
+ env = MujocoBranchable(gym.make("HalfCheetah-v5"))
16
+ ctrl = make("mppi", env, horizon=20, n_samples=200) # now branchable
17
+ action, _ = ctrl.predict(obs)
18
+
19
+ Scope
20
+ -----
21
+ Works on **any** Gymnasium MuJoCo env — the built-in ones (HalfCheetah, Ant,
22
+ Hopper, …) and your own ``MujocoEnv`` subclasses — whose complete state is the
23
+ physics state. It does **not** magically make a *non*-MuJoCo env branchable:
24
+ branching needs the full state, which an arbitrary env (hidden RNG, external
25
+ process) can't expose. If a *custom* MuJoCo env also carries Python-side state
26
+ that affects dynamics or reward (a goal, a step counter, an action history),
27
+ subclass this and extend ``get_state``/``set_state`` to include it.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import Any
33
+
34
+ import gymnasium as gym
35
+ import numpy as np
36
+
37
+
38
+ class MujocoBranchable(gym.Wrapper):
39
+ """Wrap a MuJoCo Gymnasium env so model-based controllers can branch it.
40
+
41
+ Adds ``get_state``/``set_state`` backed by MuJoCo's full physics state.
42
+ Transparent otherwise — ``reset``/``step``/spaces pass straight through.
43
+ """
44
+
45
+ def __init__(self, env: gym.Env) -> None:
46
+ super().__init__(env)
47
+ raw = env.unwrapped
48
+ if not (hasattr(raw, "model") and hasattr(raw, "data")):
49
+ raise TypeError(
50
+ "MujocoBranchable expects a MuJoCo Gymnasium env (env.unwrapped "
51
+ f"with .model/.data), got {type(raw).__name__}. Non-MuJoCo envs "
52
+ "must provide get_state/set_state themselves to be branchable."
53
+ )
54
+ import mujoco # noqa: PLC0415
55
+
56
+ self._mujoco = mujoco
57
+ self._spec = mujoco.mjtState.mjSTATE_FULLPHYSICS
58
+ self._size = int(mujoco.mj_stateSize(raw.model, self._spec))
59
+
60
+ def get_state(self) -> np.ndarray:
61
+ raw = self.env.unwrapped
62
+ arr = np.zeros(self._size, dtype=np.float64)
63
+ self._mujoco.mj_getState(raw.model, raw.data, arr, self._spec)
64
+ return arr
65
+
66
+ def set_state(self, state: Any) -> None:
67
+ raw = self.env.unwrapped
68
+ arr = np.ascontiguousarray(state, dtype=np.float64)
69
+ self._mujoco.mj_setState(raw.model, raw.data, arr, self._spec)
70
+ # Recompute derived quantities (contacts, sensors) so the restored state
71
+ # is fully consistent before the next step is planned/taken.
72
+ self._mujoco.mj_forward(raw.model, raw.data)
@@ -1,8 +1,8 @@
1
1
  """A compact, self-contained PPO (continuous actions) in PyTorch.
2
2
 
3
- SB3-like surface (``learn`` / ``predict`` / ``save`` / ``load``) but our own
4
- implementation so tau-ctrl doesn't depend on stable-baselines3. Runs on GPU
5
- automatically when torch reports CUDA available (``device="auto"``), else CPU.
3
+ Standard surface (``learn`` / ``predict`` / ``save`` / ``load``) implementation.
4
+ Runs on GPU automatically when torch reports CUDA available (``device="auto"``),
5
+ else CPU.
6
6
 
7
7
  Simulator-agnostic: talks only to the Gymnasium env's ``reset``/``step``.
8
8
  Kept intentionally small (single-env on-policy rollouts, GAE, clipped
@@ -16,12 +16,17 @@ from typing import Any
16
16
 
17
17
  import numpy as np
18
18
 
19
- from .base import BaseController, ControllerManifest, register
19
+ from .base import (
20
+ BaseController,
21
+ ControllerManifest,
22
+ register,
23
+ require_box_action_space,
24
+ require_torch,
25
+ )
20
26
 
21
27
 
22
28
  def _torch():
23
- import torch # noqa: PLC0415
24
- return torch
29
+ return require_torch()
25
30
 
26
31
 
27
32
  @register("ppo")
@@ -52,6 +57,7 @@ class PPO(BaseController):
52
57
  super().__init__(env, **kw)
53
58
 
54
59
  def _setup(self) -> None:
60
+ require_box_action_space(self.action_space, "PPO")
55
61
  torch = _torch()
56
62
  import torch.nn as nn
57
63
 
@@ -17,7 +17,13 @@ from typing import Any, Optional
17
17
 
18
18
  import numpy as np
19
19
 
20
- from .base import BaseController, ControllerManifest, register
20
+ from .base import (
21
+ BaseController,
22
+ ControllerManifest,
23
+ register,
24
+ require_box_action_space,
25
+ require_torch,
26
+ )
21
27
  from .off_policy import (
22
28
  ReplayBuffer,
23
29
  build_mlp,
@@ -29,8 +35,7 @@ from .off_policy import (
29
35
 
30
36
 
31
37
  def _torch():
32
- import torch # noqa: PLC0415
33
- return torch
38
+ return require_torch()
34
39
 
35
40
 
36
41
  @register("sac")
@@ -57,6 +62,7 @@ class SAC(BaseController):
57
62
  super().__init__(env, **kw)
58
63
 
59
64
  def _setup(self) -> None:
65
+ require_box_action_space(self.action_space, "SAC")
60
66
  torch = _torch()
61
67
  import torch.nn as nn
62
68
 
@@ -2,15 +2,15 @@
2
2
  execution strategy, dispatch. One ``Trainer.auto(...)`` call adapts to whatever
3
3
  you hand it, so tau-ctrl generalizes past any single env.
4
4
 
5
- SB3 has one execution model (CPU vector env, synchronous collect-then-update)
6
- regardless of what env you give it. The whole speed argument for tau-ctrl is
7
- that wall-clock-to-reward is bounded by the *slower* of collection and update,
8
- and which one dominates depends on the env — so a general framework must detect
9
- the regime and route to the matching engine:
5
+ Standard frameworks typically support a single execution model (e.g., CPU vector envs,
6
+ synchronous collect-then-update) regardless of the target environment. The whole
7
+ speed argument for tau-ctrl is that wall-clock-to-reward is bounded by the *slower* of
8
+ collection and update, and which one dominates depends on the env — so a general
9
+ framework must detect the regime and route to the matching engine:
10
10
 
11
11
  ON_DEVICE_VECTORIZED env already batched + on GPU (MJX/Brax via dlpack,
12
- Isaac Gym, or a native TorchVecEnv) → 10-100x, the
13
- real win; SB3 can't do this at all.
12
+ Isaac Gym, or a native TorchVecEnv) → 10-100x speedup
13
+ by running everything on-device.
14
14
  GYM_VECTOR_ADAPTED a gymnasium.vector.VectorEnv (numpy/CPU) → batched
15
15
  update on device, collection stays CPU.
16
16
  SYNC_VEC a single non-batchable env replicated N times, stepped
@@ -192,7 +192,7 @@ def select_strategy(
192
192
  why = (
193
193
  f"Env is a native on-device TorchVecEnv ({ec.backend}, on_device={ec.on_device}). "
194
194
  "Best case: env stepping AND the gradient update run batched on the target "
195
- "device — the regime where tau-ctrl beats SB3 10-100x (SB3's VecEnv can't). "
195
+ "device — the regime that maximizes GPU throughput by avoiding CPU-GPU synchronization. "
196
196
  f"Scaling updates_per_step→{ups} so {n} fresh transitions/step actually train."
197
197
  )
198
198
  return Plan(Strategy.ON_DEVICE_VECTORIZED, n, dev, ups, why)
@@ -225,7 +225,7 @@ def select_strategy(
225
225
 
226
226
  why = (
227
227
  "Single env, not replicable (e.g. a real robot). Physics can't be parallelised — "
228
- "only the gradient update is GPU-accelerated. Honest regime: beat SB3 here on "
228
+ "only the gradient update is GPU-accelerated. Honest regime: focus on "
229
229
  "sample efficiency (fewer env steps to reward), not raw throughput. Use SAC/TD3."
230
230
  )
231
231
  return Plan(Strategy.SINGLE_ENV, 1, dev, 1, why)
@@ -13,7 +13,13 @@ from typing import Any
13
13
 
14
14
  import numpy as np
15
15
 
16
- from .base import BaseController, ControllerManifest, register
16
+ from .base import (
17
+ BaseController,
18
+ ControllerManifest,
19
+ register,
20
+ require_box_action_space,
21
+ require_torch,
22
+ )
17
23
  from .off_policy import (
18
24
  ReplayBuffer,
19
25
  build_mlp,
@@ -25,8 +31,7 @@ from .off_policy import (
25
31
 
26
32
 
27
33
  def _torch():
28
- import torch # noqa: PLC0415
29
- return torch
34
+ return require_torch()
30
35
 
31
36
 
32
37
  @register("td3")
@@ -57,6 +62,7 @@ class TD3(BaseController):
57
62
  super().__init__(env, **kw)
58
63
 
59
64
  def _setup(self) -> None:
65
+ require_box_action_space(self.action_space, "TD3")
60
66
  torch = _torch()
61
67
  import torch.nn as nn
62
68
 
@@ -1,9 +1,9 @@
1
1
  """GPU-vectorized environments — the piece that unlocks real GPU speedup.
2
2
 
3
- SB3 (and tau-ctrl's single-env ``learn()``) step one CPU environment at a
4
- time: one transition, one tiny update, ~100 sequential CUDA kernel launches
5
- per step. At that granularity kernel-launch latency dwarfs the actual math,
6
- so "putting the network on the GPU" caps out at ~1.3x.
3
+ Single-environment training loops (and tau-ctrl's single-env ``learn()``) step
4
+ one CPU environment at a time: one transition, one tiny update, ~100 sequential
5
+ CUDA kernel launches per step. At that granularity kernel-launch latency dwarfs
6
+ the actual math, so "putting the network on the GPU" caps out at ~1.3x.
7
7
 
8
8
  A :class:`TorchVecEnv` instead advances ``num_envs`` environments *in
9
9
  parallel*, entirely as batched tensor ops on the target device. Nothing
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tau-ctrl
3
- Version: 0.1.0
4
- Summary: SB3-like, simulator-agnostic control algorithms over any gymnasium.Env — PID, MPPI/CEM/iCEM, iLQR, CBF, and GPU-native RL (PPO/SAC/TD3) with vectorized on-device training
3
+ Version: 0.2.0
4
+ Summary: Simulator-agnostic control algorithms over any gymnasium.Env — PID, MPPI/CEM/iCEM, iLQR, CBF, and GPU-native RL (PPO/SAC/TD3) with vectorized on-device training
5
5
  Author: Tau Intelligence
6
6
  License: Apache-2.0
7
7
  Project-URL: Homepage, https://tau-intelligence.com/
@@ -40,9 +40,9 @@ Dynamic: license-file
40
40
 
41
41
  # tau-ctrl
42
42
 
43
- **SB3-like, simulator-agnostic control algorithms — feedback, sampling-based MPC, safety filtering, and GPU-native RL behind one interface.**
43
+ **Simulator-agnostic control algorithms — feedback, sampling-based MPC, safety filtering, and GPU-native RL behind one interface.**
44
44
 
45
- Like Stable-Baselines3, but for the whole controller spectrum, over any `gymnasium.Env`. No simulator dependency: works with whatever hands you an env (e.g. [tau-sim](https://tau-intelligence.com/tau_sim/)). Unlike SB3, the RL methods train **on-device** and scale to vectorized environments for real GPU speedup.
45
+ A unified library for the whole controller spectrum, over any `gymnasium.Env`. No simulator dependency: works with whatever hands you an env (e.g. [tau-sim](https://tau-intelligence.com/tau_sim/)). The RL methods train **on-device** and scale to vectorized environments for real GPU speedup.
46
46
 
47
47
  ## Installation
48
48
 
@@ -57,7 +57,7 @@ pip install tau-ctrl[torch] # + RL: PPO, SAC, TD3, and vectorized on-device
57
57
  from tau_ctrl import make
58
58
 
59
59
  ctrl = make("mppi", env, horizon=25, n_samples=300) # or MPPI(env, ...), SAC(env), ...
60
- action, _ = ctrl.predict(obs) # SB3-style
60
+ action, _ = ctrl.predict(obs) # standard interface
61
61
  ctrl.learn(total_timesteps=100_000) # trainable methods (ppo/sac/td3)
62
62
  ctrl.save("ctrl.pkl")
63
63
  ```
@@ -84,7 +84,7 @@ seeding of torch's RNG).
84
84
 
85
85
  ## GPU-native RL: vectorized, on-device training
86
86
 
87
- SB3 steps CPU environments and the policy update sits behind per-step Python.
87
+ Standard pipelines step CPU environments sequentially and perform updates in slow Python loops.
88
88
  tau-ctrl's SAC/TD3 instead run the replay buffer and the update **on the target
89
89
  device**, and — given a batched env — step thousands of environments in parallel
90
90
  with no numpy in the hot loop. `Trainer.auto` probes your env and hardware and
@@ -108,8 +108,7 @@ action, _ = model.predict(obs)
108
108
  | a single, non-batchable env (PyBullet, classic MuJoCo) + a factory | `SyncTorchVecEnv` | batched update on device |
109
109
  | one env you can't replicate (a real robot) | — (single-env path) | only the update |
110
110
 
111
- See [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md) for head-to-head numbers vs
112
- Stable-Baselines3 and skrl, and [`examples/`](examples/) for runnable scripts
111
+ See [`benchmarks/RESULTS.md`](benchmarks/RESULTS.md) for performance comparison benchmarks, and [`examples/`](examples/) for runnable scripts
113
112
  (`quickstart.py`, `vectorized_rl.py`, `adaptive_training.py`).
114
113
 
115
114
  ## Safety filtering
@@ -151,4 +150,3 @@ Apache 2.0 — see [LICENSE](LICENSE).
151
150
  ## Related
152
151
 
153
152
  - [tau-sim](https://tau-intelligence.com/tau_sim/) — robotics environment builder
154
- - [Stable-Baselines3](https://stable-baselines3.readthedocs.io/) — RL algorithms (API inspiration)
@@ -14,6 +14,7 @@ src/tau_ctrl/algorithms/base.py
14
14
  src/tau_ctrl/algorithms/cbf.py
15
15
  src/tau_ctrl/algorithms/ilqr.py
16
16
  src/tau_ctrl/algorithms/mppi.py
17
+ src/tau_ctrl/algorithms/mujoco.py
17
18
  src/tau_ctrl/algorithms/off_policy.py
18
19
  src/tau_ctrl/algorithms/pid.py
19
20
  src/tau_ctrl/algorithms/ppo.py
File without changes
File without changes
File without changes