RLBotics
PPO for robot learning, in PyTorch. Train a policy on a simulator, watch the curves in TensorBoard, export it to a single ONNX file, and run it on the robot with numpy alone.
Three simulators are supported, all through the same runner: Gymnasium (works everywhere, good for getting started), mjlab (MuJoCo Warp on the GPU), and Isaac Lab (Isaac Sim).
env = GymnasiumVecEnv("Hopper-v5", num_envs=32, device="auto")
runner = OnPolicyRunner(env=env, runner_cfg=cfg, device="auto")
runner.learn(num_learning_iterations=1500)
path = runner.export() # one self-contained policy.onnx
action = Policy(path).get_action(observation) # deployment: numpy in, actions out
Requirements
- Python 3.10–3.12
- PyTorch with CUDA build:
pip install torch --index-url https://download.pytorch.org/whl/cu128 # match your CUDA toolkit
Install
git clone -b develop git@gitlab.com:telekinesis/rlbotics.git
cd rlbotics
Choose any of the following installations:
| Extra | Install | What it adds | Runs on |
|---|---|---|---|
| (none) | pip install -e . |
The library: PPO, runner, models, logging, ONNX export | anywhere |
gym |
pip install -e ".[gym]" |
Gymnasium classic-control and MuJoCo tasks | macOS, Linux, Windows |
mjlab |
pip install -e ".[mjlab]" then pip install "mjlab[cu128]" |
mjlab tasks on MuJoCo Warp | Linux/Windows + NVIDIA GPU (macOS: mjlab[cpu], evaluation only) |
isaaclab |
pip install -e ".[isaaclab]" --extra-index-url https://pypi.nvidia.com |
Isaac Lab tasks on Isaac Sim | Linux/Windows + NVIDIA GPU, Python 3.11 |
examples |
pip install -e ".[examples]" |
gym plus onnxruntime, needed to run the exported policy |
anywhere |
dev |
pip install -e ".[dev]" |
ruff, pylint, pytest | anywhere |
The Isaac Sim wheels live on NVIDIA's index, so that extra only resolves with
--extra-index-url https://pypi.nvidia.com. There is no macOS build of Isaac Sim.
Quickstart
pip install -e ".[examples]"
python examples/gymnasium_example.py # Humanoid-v5, roughly 20 minutes
tensorboard --logdir logs # watch it learn
That teaches the MuJoCo humanoid — 17 actuators, a 348-dimensional observation — to stand and walk,
exports the policy to ONNX, then runs the exported file. Measured at the defaults, mean reward goes
from 60 to 622 and mean episode length from 13 to 129 steps over 600 iterations. Running is a
millions-of-steps problem, so the default 3000 iterations is a start, and --resume continues from
the newest checkpoint:
python examples/gymnasium_example.py --resume
Want a result in two minutes instead? Pendulum solves quickly, and the same script does it:
python examples/gymnasium_example.py -e Pendulum-v1 -n 8 -s 128 -i 120 \
--learning-rate 3e-4 --epochs 10 --entropy-coef 0.01
Measured: mean reward from about -1080 to -211, a solved swing-up. Watch
Episodes/mean_length first on the humanoid — staying upright is what the return is built on — and
Diagnostics/kl with Diagnostics/clip_fraction to check the updates are sane.
Examples
| Example | What it does | Needs |
|---|---|---|
| gymnasium_example.py | Train, export and deploy on any Gymnasium continuous control task | [examples] |
| mjlab_example.py | The same, on an mjlab task | [mjlab] |
| isaaclab_example.py | The same, on an Isaac Lab task | [isaaclab] |
| module_examples/ | One library piece at a time: configs, MLP, CNN, distributions, normalization, rollout buffer, logger | (none) |
| run_all_examples.py | Runs every example as a smoke test; reports SKIP when an optional dependency is missing | (none) |
Every training example takes --help, and shares the same arguments: -i iterations, -n
environments, -s steps per environment, -d device, --resume, --record-video,
--experiment-name, --log-dir.
Environments
Gymnasium
Any continuous control task works from its id alone — the observation size, actuator count, action bounds and episode limit are all read from the environment:
python examples/gymnasium_example.py -e Ant-v5 -n 32 -s 64 -i 2000 --epochs 5
| Task | Observation | Actions | Action range | Notes |
|---|---|---|---|---|
Pendulum-v1 |
3 | 1 | ±2.0 | Quickest check; solved in ~120 iterations |
MountainCarContinuous-v0 |
2 | 1 | ±1.0 | Sparse reward, needs exploration |
InvertedPendulum-v5 |
4 | 1 | ±3.0 | Easiest MuJoCo task |
InvertedDoublePendulum-v5 |
9 | 1 | ±1.0 | |
Reacher-v5 |
10 | 2 | ±1.0 | 50-step episodes |
Swimmer-v5 |
8 | 2 | ±1.0 | |
Hopper-v5 |
11 | 3 | ±1.0 | Locomotion, terminates on falling |
Walker2d-v5 |
17 | 6 | ±1.0 | |
HalfCheetah-v5 |
17 | 6 | ±1.0 | Never terminates |
Pusher-v5 |
23 | 7 | ±2.0 | Manipulation |
Ant-v5 |
105 | 8 | ±1.0 | Use wider networks |
Humanoid-v5 |
348 | 17 | ±0.4 | The default. Hardest; millions of steps to run |
The MuJoCo tasks come with [gym]. Networks default to (256, 256, 128) above 100 observation
dimensions and (64, 64) below, so Ant-v5 and Humanoid-v5 get the capacity they need without
being told. The defaults elsewhere (learning rate 1e-4, 2 epochs, entropy 0.0011) suit a
many-actuator task; smaller ones want a larger rate, more epochs and more entropy, as in the recipes
at the top of gymnasium_example.py.
Discrete tasks (CartPole-v1, Acrobot-v1, MountainCar-v0) are rejected with a clear error:
the policy is Gaussian, so it needs a continuous action space. Box2D tasks
(LunarLanderContinuous-v3, BipedalWalker-v3) would work, but gymnasium[box2d] is not in the
extra — pip install "gymnasium[box2d]" to use them.
mjlab
GPU-parallel MuJoCo with Isaac Lab's manager-based API. Tasks publish an actor group and a
privileged critic group, which the example wires up automatically. All 12 registered tasks:
| Task | What it is |
|---|---|
Mjlab-Velocity-Flat-Unitree-G1 |
Humanoid velocity tracking on flat ground (the default) |
Mjlab-Velocity-Rough-Unitree-G1 |
The same on rough terrain |
Mjlab-Velocity-Flat-Unitree-Go1 |
Quadruped velocity tracking, flat |
Mjlab-Velocity-Rough-Unitree-Go1 |
Quadruped velocity tracking, rough |
Mjlab-Tracking-Flat-Unitree-G1 |
Humanoid motion imitation |
Mjlab-Tracking-Flat-Unitree-G1-No-State-Estimation |
The same without state estimation |
Mjlab-Cartpole-Balance |
Cartpole balance — cheap enough to smoke-test on CPU |
Mjlab-Cartpole-Swingup |
Cartpole swing-up |
Mjlab-Lift-Cube-Yam |
Arm lifting a cube |
Mjlab-Lift-Cube-Yam-Rgb |
The same from RGB images |
Mjlab-Lift-Cube-Yam-Depth |
The same from depth images |
Mjlab-Multi-Cube-Seg-Yam |
Multi-cube manipulation with segmentation |
python examples/mjlab_example.py --list-tasks
python examples/mjlab_example.py -t Mjlab-Cartpole-Balance -n 8 -s 16 -i 3 -d cpu # CPU smoke run
python examples/mjlab_example.py -t Mjlab-Velocity-Flat-Unitree-G1 -n 4096 -i 3000 # needs a GPU
mjlab task configs ship num_envs=1, so always pass -n. The vision tasks publish a camera group
the example ignores, since it trains an MLP.
Isaac Lab
Isaac Sim, with the largest task library of the three. --list-tasks prints the authoritative list
for your install; a representative selection:
| Category | Tasks |
|---|---|
| Classic | Isaac-Cartpole-v0, Isaac-Ant-v0, Isaac-Humanoid-v0 (each also -Direct-v0) |
| Quadruped locomotion | Isaac-Velocity-Flat-Anymal-C-v0, Isaac-Velocity-Rough-Anymal-C-v0, and the same for Anymal B/D, Unitree A1/Go1/Go2, Spot |
| Humanoid locomotion | Isaac-Velocity-Flat-H1-v0, Isaac-Velocity-Flat-G1-v0, Digit variants |
| Manipulation | Isaac-Reach-Franka-v0, Isaac-Lift-Cube-Franka-v0, Isaac-Stack-Cube-Franka-v0, Isaac-Open-Drawer-Franka-v0, Isaac-Repose-Cube-Allegro-v0 |
python examples/isaaclab_example.py --list-tasks
python examples/isaaclab_example.py -t Isaac-Cartpole-v0 -n 64 -i 50
python examples/isaaclab_example.py -t Isaac-Velocity-Flat-Anymal-C-v0 -n 4096 -i 1500
Isaac Lab tasks publish a policy group and, when asymmetric, a critic group. Isaac Sim is
launched by the adapter before the task is built, so nothing from isaaclab_tasks may be imported
before that.
Your own environment
Nothing about the library is tied to a simulator. Implement
VecEnv — observations as a TensorDict of named groups,
rewards and dones shaped (num_envs,), truncations under extras["time_outs"] — and the runner
trains against it. The three adapters in envs/ are each about 200
lines and are the reference.
Training runs
Everything a run produces goes in one directory, logs/<experiment>/<timestamp>/:
logs/gymnasium_ppo/2026-08-06_18-08-47/
├── config.json the full config the run used
├── events.out.* TensorBoard scalars
├── model_100.pt checkpoints, rotated to keep the last N
├── model_100.mp4 with --record-video, the rollout that produced that checkpoint
├── model_best.pt the highest mean reward of the run, never rotated away
└── policy.onnx the exported policy, which is the best checkpoint
model_best.pt is rewritten whenever the mean episode reward beats every earlier iteration's, and it
is checked every iteration rather than on the save_interval grid, so a peak that the reward later
falls back from is not lost. It records what it scored and when, which is how --resume best
compares runs.
--record-video writes an MP4 beside every checkpoint, rendered from that iteration's own rollout —
no second simulator, one render per step on the iterations that checkpoint. A reward curve says a
policy improved; the clip says whether it is walking or shuffling on one knee. The environment has to
be built to render, which the examples do when the flag is set, and rotation deletes a video with the
checkpoint it belongs to.
--resume continues from an earlier checkpoint and writes a fresh timestamped directory, so a
resumed run never overwrites the one it continued from. It takes one value, and searches every run of
the experiment:
--resume |
the most recently written checkpoint (same as --resume last) |
--resume best |
the highest-scoring checkpoint of the experiment, which may be in an older run |
--resume model_500.pt |
that file, found among the experiment's runs |
--resume path/to/model.pt |
exactly that file, no searching |
The best score travels inside the checkpoint, so a run that continues from a strong policy and then
does worse cannot demote it. Alongside the usual reward
and loss curves, Diagnostics/kl, Diagnostics/clip_fraction and Diagnostics/explained_variance
are what tell you whether an update was sane — a reward that climbs then collapses is almost always
KL far above target.
Symmetry
A legged robot is left-right symmetric, so a policy that has learned to trot leading with one leg has in principle learned the mirrored gait too. Telling PPO about that is worth real sample efficiency and is what stops a policy settling into a limp: mirrored samples are appended to every mini-batch, and an optional term penalizes the policy for disagreeing with itself on them. This follows Mittal et al., ICRA 2024, and the flow matches rsl_rl's.
It is off by default and configured in Python rather than from the command line, because the one piece it needs cannot be shipped: a mirror function saying which observation entry mirrors which, which only somebody who knows the robot's joint order can write.
from telekinesis.rlbotics.config import PPOConfig, SymmetryConfig
PPOConfig(
...,
symmetry_cfg=SymmetryConfig(
# The function, or an import path to it so the config survives config.json
data_augmentation_func="my_robot.symmetry:mirror",
use_data_augmentation=True, # mirrored samples in every mini-batch
use_mirror_loss=True, # the auxiliary term
mirror_loss_coeff=1.0,
),
)
The function is called as func(env=env, obs=obs, actions=actions) — the environment is passed in, so
it can read the observation layout — and returns each argument as the originals stacked with their
mirrored copies along the batch dimension. Either argument may be None. Setting both flags to False
still computes and reports the loss, detached, which is a cheap way to watch how symmetric a policy is
without changing what it optimizes. Diagnostics-style reporting puts it in the loss table as
symmetry.
Writing one
For a manager-based environment (mjlab, Isaac Lab) the layout is metadata you can read, not something to guess:
env.venv.observation_manager.active_terms # term names per group
env.venv.observation_manager.group_obs_term_dim # their dimensions, so you get each term's offsets
env.venv.scene["robot"].joint_names # carries the side and the axis
For the Unitree G1's 99-dimensional actor group that yields base_lin_vel[0:3],
base_ang_vel[3:6], projected_gravity[6:9], joint_pos[9:38], joint_vel[38:67],
actions[67:96], command[96:99]. Each term then follows from what kind of quantity it is:
| Term | Under a left-right mirror |
|---|---|
base_lin_vel, projected_gravity |
polar vectors: (+, −, +), the lateral component flips |
base_ang_vel |
pseudovector: (−, +, −), the in-plane components flip, the normal one does not |
command (vx, vy, yaw rate) |
(+, −, −) |
joint_pos, joint_vel, actions |
swap left↔right joints, negate those whose axis flips |
| per-foot terms (height, air time, contact) | swap the two feet |
| contact forces | swap the feet, negate the lateral component |
The joint part is derivable rather than typed: names like left_hip_roll_joint give both the partner
(left_↔right_) and the sign (negate when the axis is roll or yaw). For the G1 that produces 26
swapped and 16 negated entries across 29 joints.
Verifying one
Do not trust a mirror you have not measured. A wrong one trains happily and teaches an invariance the robot does not have — nothing about the run looks wrong. Three checks, cheapest first:
- Involution:
mirror(mirror(x)) == x. Catches a bad permutation or a stray sign at once. - Commutes with the dynamics: mirror the simulation state as well, step both with mirrored actions, and confirm the next observation mirrors and the reward is unchanged. This is the real test. Note that observation noise and randomizing reset events have to be off for it to mean anything, and that rough-terrain tasks are not mirror-symmetric in their dynamics even when the observation map is right.
- During training: the reported
symmetryloss should start O(1) with a fresh policy and fall. Pinned at zero from the first iteration means your mirror is the identity somewhere.
Measured this way over 200 random states, three Gymnasium tasks have exact-enough maps — a sign flip per entry, no permutation:
| Task | Observation error | Reward error |
|---|---|---|
Pendulum-v1, obs (+, −, −), action (−) |
exact | exact |
InvertedDoublePendulum-v5 |
1e-06 | 1e-10 |
InvertedPendulum-v5 |
4e-03, solver noise: it grows with derivative order | exact |
Swimmer-v5 was measured too and is not symmetric under a sign flip (0.4 on observations, 1.0 on
the reward), which is a good illustration of why the measurement matters — it looks like it should be.
Humanoid and the walkers are left-right symmetric, but their mirror is a joint permutation over body
blocks (cinert, cvel, cfrc_ext), not a sign flip.
Deployment
runner.export() writes one self-contained policy.onnx holding the run's best checkpoint,
not whichever policy training happened to end on — those differ whenever the reward peaked and fell
back, which for a long locomotion run is the normal case. Inside the graph are the observation
normalization the policy trained with, the deterministic action, and, for environments with action
bounds, the scaling onto them. Running it needs neither PyTorch nor this library:
from telekinesis.rlbotics.policy import Policy
policy = Policy("logs/gymnasium_ppo/2026-08-06_18-08-47/policy.onnx")
action = policy.get_action(observation) # (obs_dim,) -> (num_actions,), or batched
Only numpy and onnxruntime. The batch dimension is dynamic, so one observation or a batch both work.
runner.export(from_best=False) ships the final policy instead, for comparing where training ended
up against the best it found.
Verification status
| Path | State |
|---|---|
| Library: PPO, runner, models, config, logging, checkpoints, video, ONNX export | 295 tests |
| Gymnasium adapter and example | Trained and deployed on Pendulum, Hopper, Reacher, MountainCarContinuous, Humanoid |
| mjlab adapter and example | Trained and deployed on CPU (Mjlab-Cartpole-Balance, Mjlab-Cartpole-Swingup) and on GPU across the velocity-tracking tasks |
| Isaac Lab adapter and example | Trained and deployed on GPU across the classic, quadruped and humanoid locomotion tasks |
| CUDA | Verified on an NVIDIA GPU across all three adapters |
Hyperparameters in gymnasium_example.py are measured. Those in the mjlab and Isaac Lab examples are
the usual starting points for legged locomotion, not measured recipes.
Development
pip install -e ".[dev,examples]"
pytest # 295 tests
ruff check .
python examples/run_all_examples.py # smoke-test every example
License
Apache 2.0, see LICENSE. Copyright Telekinesis GmbH.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file telekinesis_rlbotics-0.1.2.tar.gz.
File metadata
- Download URL: telekinesis_rlbotics-0.1.2.tar.gz
- Upload date:
- Size: 111.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
147d49ef3998300e718d8677298cc290b737d9ba2ecb52dbbe40039b70ab84fe
|
|
| MD5 |
869a8cdeb5ac03411e4ea342a99fe474
|
|
| BLAKE2b-256 |
3a92a0fe87e7187c818c51aaba14bb95e6636a115efd6674b3db4156ac72c0d5
|
Provenance
The following attestation bundles were made for telekinesis_rlbotics-0.1.2.tar.gz:
Publisher:
release.yml on telekinesis-ai/telekinesis-rlbotics
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
telekinesis_rlbotics-0.1.2.tar.gz -
Subject digest:
147d49ef3998300e718d8677298cc290b737d9ba2ecb52dbbe40039b70ab84fe - Sigstore transparency entry: 2419310861
- Sigstore integration time:
-
Permalink:
telekinesis-ai/telekinesis-rlbotics@2ef393c15c43d04dac3881a1df6f4615e0f3d5a0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/telekinesis-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2ef393c15c43d04dac3881a1df6f4615e0f3d5a0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file telekinesis_rlbotics-0.1.2-py3-none-any.whl.
File metadata
- Download URL: telekinesis_rlbotics-0.1.2-py3-none-any.whl
- Upload date:
- Size: 80.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
602c480671cf0a8264d207413ada26007fc24cdf162f1c96b64a96c45b9fa09b
|
|
| MD5 |
b4acd7f21b033d77b1e4907552529d52
|
|
| BLAKE2b-256 |
299e0a1b952abeaad550950bf8a57c3097a18a546dadd7ea3d2d68a969870916
|
Provenance
The following attestation bundles were made for telekinesis_rlbotics-0.1.2-py3-none-any.whl:
Publisher:
release.yml on telekinesis-ai/telekinesis-rlbotics
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
telekinesis_rlbotics-0.1.2-py3-none-any.whl -
Subject digest:
602c480671cf0a8264d207413ada26007fc24cdf162f1c96b64a96c45b9fa09b - Sigstore transparency entry: 2419311761
- Sigstore integration time:
-
Permalink:
telekinesis-ai/telekinesis-rlbotics@2ef393c15c43d04dac3881a1df6f4615e0f3d5a0 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/telekinesis-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2ef393c15c43d04dac3881a1df6f4615e0f3d5a0 -
Trigger Event:
push
-
Statement type: