Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ _build

# LSP
pyrightconfig.json
.venvs/
56 changes: 34 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,37 +46,49 @@ The table below lists all available environments:
>
> Discuss in [Github Discussion](https://github.com/fan-ziqi/robot_lab/discussions) or [Discord](http://www.robotsfan.com/dc_robot_lab).

## Version Dependency
## Frameworks

| robot_lab Version | Isaac Lab Version | Isaac Sim Version |
|------------------ | ----------------------------- | ------------------------- |
| `main` branch | `main` branch | Isaac Sim 4.5 / 5.0 / 5.1 |
| `v2.3.2` | `v2.3.2` | Isaac Sim 4.5 / 5.0 / 5.1 |
| `v2.2.2` | `v2.2.1` | Isaac Sim 4.5 / 5.0 |
| `v2.1.1` | `v2.1.1` | Isaac Sim 4.5 |
| `v1.1` | `v1.4.1` | Isaac Sim 4.2 |
robot_lab tasks run on either **IsaacLab** or **mjlab**. Select the backend
at runtime via the dispatcher:

## Installation
```bash
python scripts/train.py --framework isaaclab --rl rsl_rl --task RobotLab-Velocity-Rough-Unitree-A1-v0
python scripts/train.py --framework mjlab --rl rsl_rl --task RobotLab-Velocity-Rough-Unitree-A1-v0
```

- Install Isaac Lab by following the [installation guide](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html). We recommend using the conda installation as it simplifies calling Python scripts from the terminal.
Each backend has its own venv, provisioned by `./setup_env.sh`. The
dispatcher auto-`exec`s under the right interpreter — no `source ... activate`
needed. Tasks tagged `framework_required="isaaclab"` are silently skipped
on the mjlab backend (and vice versa); running them with the wrong
`--framework` produces a clear "task not registered" error.

- Clone this repository separately from the Isaac Lab installation (i.e. outside the `IsaacLab` directory):
Existing IsaacLab users should read
[`docs/migration-from-subclass.md`](docs/migration-from-subclass.md)
for the (small) set of changes they need to make.

```bash
git clone https://github.com/fan-ziqi/robot_lab.git
```
## Version Dependency

- Using a python interpreter that has Isaac Lab installed, install the library
| robot_lab Version | Isaac Lab Version | Isaac Sim Version | mjlab Version |
|------------------ | ----------------------------- | ------------------------- | ------------- |
| `main` branch | `main` branch | Isaac Sim 4.5 / 5.0 / 5.1 | `>=1.4,<2` |
| `v2.3.2` | `v2.3.2` | Isaac Sim 4.5 / 5.0 / 5.1 | n/a |
| `v2.2.2` | `v2.2.1` | Isaac Sim 4.5 / 5.0 | n/a |
| `v2.1.1` | `v2.1.1` | Isaac Sim 4.5 | n/a |
| `v1.1` | `v1.4.1` | Isaac Sim 4.2 | n/a |

```bash
python -m pip install -e source/robot_lab
```
## Installation

- Verify that the extension is correctly installed by running the following command to print all the available environments in the extension:
Pick one (or both, in separate venvs):

```bash
python scripts/tools/list_envs.py
```
```bash
./setup_env.sh isaaclab # Python 3.11, IsaacSim+IsaacLab via PyPI
./setup_env.sh mjlab # Python 3.12, mjlab via PyPI
```

The two venvs live at `.venvs/{isaaclab,mjlab}/` and are managed by uv. You do
not need to `source ... activate` — the dispatcher (`scripts/train.py`,
`scripts/play.py`) auto-exec's under the right interpreter when you pass
`--framework`.

<details>

Expand Down
198 changes: 198 additions & 0 deletions docs/migration-from-subclass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Migrating from the IsaacLab subclass style

This guide is for IsaacLab users who maintain custom tasks in robot_lab and
want to keep them working after the dual-framework migration.

**TL;DR:** existing custom tasks (subclassing `LocomotionVelocityRoughEnvCfg`,
overriding fields in `__post_init__`) continue to work on IsaacLab unchanged.
Only two things break:

1. **Task IDs** dropped the `Isaac-` infix:
`RobotLab-Isaac-Velocity-Rough-Unitree-A1-v0` → `RobotLab-Velocity-Rough-Unitree-A1-v0`.
Update shell aliases / CI / wandb run names.
2. **Log root** moved from `logs/{rsl_rl,skrl,cusrl}/<exp>` to
`logs/isaaclab/<exp>`. Old wandb / tensorboard run links no longer
resolve; new runs work normally.

## Quick start

```bash
# Old:
python scripts/reinforcement_learning/rsl_rl/train.py \
--task RobotLab-Isaac-Velocity-Rough-Unitree-A1-v0 ...

# New:
python scripts/train.py --framework isaaclab --rl rsl_rl \
--task RobotLab-Velocity-Rough-Unitree-A1-v0 ...
```

The dispatcher (`scripts/train.py`, `scripts/play.py`) auto-`exec`s under
`.venvs/isaaclab/bin/python`, so you don't need to source-activate first.

## What changed and why

The robot_lab repo now supports both **IsaacLab** and **mjlab** as simulator
backends. A new `robot_lab.framework` adapter package re-exports cfg types,
sensor/event factories, MDP function aliases, and the task-registration entry
point. Business code imports from `robot_lab.framework`; the framework module
chooses the active backend at first import via the `ROBOT_LAB_FRAMEWORK`
environment variable (set by the dispatcher).

For IsaacLab users, this changes very little:

| Aspect | Before | After |
|---|---|---|
| `import isaaclab.managers` etc. in your code | OK | Use `robot_lab.framework` re-exports |
| `gym.register(id="RobotLab-Isaac-Velocity-...")` | OK | `register_task(task_id="RobotLab-Velocity-...", framework_required="isaaclab")` |
| `LocomotionVelocityRoughEnvCfg` subclass + `__post_init__` overrides | OK | Same (kept untouched) |
| `from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg` in `agents/` | OK | Same — agent files still import directly; `--framework mjlab` skips them via `framework_required` |

If you have a custom task following the existing pattern, the only change you
must make is:

1. Strip `Isaac-` from your task ID(s).
2. Replace the `gym.register(id="...", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs=...)`
call with:

```python
from robot_lab.framework import register_task

register_task(
task_id="RobotLab-Velocity-Rough-MyRobot-v0",
env_cfg=f"{__name__}.rough_env_cfg:MyRobotRoughEnvCfg",
agent_cfg_entries={
"rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:MyRobotRoughPPORunnerCfg",
},
framework_required="isaaclab",
)
```

That's it. Your env_cfg subclass, your `__post_init__` overrides, your
agent cfgs — all stay verbatim.

## Why `framework_required="isaaclab"`

`LocomotionVelocityRoughEnvCfg` inherits the IL-style nested
`@configclass` pattern. Under the mjlab backend, the class body's
`ObservationGroupCfg(...)` with no `terms=` argument fails because mjlab's
`ObservationGroupCfg` requires that field. So the subclass is IL-only by
design until someone authors a mjlab-native env cfg from scratch (Phase 7+
of the migration plan).

`framework_required="isaaclab"` makes `register_task` silently skip the
registration when running under `--framework mjlab`. mjlab users see your
task as "not registered" rather than as a class-evaluation crash.

## Adding a NEW task that should work on BOTH backends

This is the new path enabled by the migration. Use the `framework` factories:

```python
from robot_lab.framework import (
EnvCfg,
SceneEntityCfg,
RewardTermCfg,
EventTermCfg,
ObservationTermCfg,
manager_container,
make_observation_group,
make_height_scan_sensor,
make_contact_sensor,
make_rough_terrain,
make_sky_light,
push_robot,
randomize_geom_friction,
register_task,
mdp, # framework-routed mdp aliases
)


def my_robot_rough_env_cfg() -> EnvCfg:
rewards = manager_container({
"track_lin_vel_xy_exp": RewardTermCfg(
func=mdp.track_lin_vel_xy_exp,
weight=3.0,
params={"command_name": "base_velocity", "std": 0.5},
),
})
events = manager_container({
"push_robot": push_robot(velocity_range={"x": (-0.5, 0.5)}),
})
# ... assemble scene, observations, actions, terminations, etc.
cfg = EnvCfg(
scene=...,
rewards=rewards,
events=events,
...,
)
return cfg


register_task(
task_id="RobotLab-Velocity-Rough-MyRobot-v0",
env_cfg=my_robot_rough_env_cfg,
framework_required="any", # registers on both backends
)
```

`framework_required="any"` (the default) registers the task on both
backends. The factory function returns `EnvCfg` which resolves to
`isaaclab.envs.ManagerBasedRLEnvCfg` under IL and
`mjlab.envs.ManagerBasedRlEnvCfg` under mjlab; same with `RewardTermCfg`,
`EventTermCfg`, etc.

## When backend-specific helpers don't exist

Some MDP terms exist only on IsaacLab (e.g. `joint_deviation_l1`,
`applied_torque_limits`, `undesired_contacts`, `feet_air_time_positive_biped`).

Two options:

1. **Mark the whole task IL-only:** `framework_required="isaaclab"`.
2. **Branch on framework at cfg-build time:**

```python
from robot_lab.framework import current_framework_supports

rewards = manager_container({
"track_lin_vel_xy_exp": RewardTermCfg(
func=mdp.track_lin_vel_xy_exp, weight=3.0,
params={"command_name": "base_velocity", "std": 0.5},
),
**({
"joint_deviation_l1": RewardTermCfg(...), # IL-only term
} if current_framework_supports("isaaclab") else {}),
})
```

## File layout reference

- `robot_lab.framework` — public adapter API. Cfg types, factories, MDP aliases.
- `robot_lab.framework.{isaaclab,mjlab}.*` — backend-specific implementations.
- `robot_lab.assets` — robot definitions. New `assets/data/<robot>.py`
carries framework-neutral constants (paths, init pose, gains).
- `robot_lab.assets.builders.{isaaclab,mjlab}` — `build_articulation_cfg` /
`build_entity_cfg` consume the neutral data and return the right cfg type.
- `robot_lab.tasks.manager_based.locomotion.velocity.velocity_env_cfg` —
IL-style nested `@configclass` base; per-robot subclasses still inherit it.
- `scripts/{train,play}.py` — top-level dispatcher.
- `scripts/{isaaclab,mjlab}/...` — backend-specific entry points.

## Frequently surprising things

- `import robot_lab` works under both venvs even when nothing else loads
(`tasks/__init__.py` swallows `ImportError`s during task registration).
- Under the mjlab venv, `import robot_lab.assets.unitree` succeeds but the
module-level `UNITREE_GO2_CFG` resolves to `None` because the IL-side
cfg classes aren't importable. Use the `build_unitree_a1_cfg()`-style
factory instead, which dispatches on `get_framework()`.
- The mjlab CLI is **tyro**, not argparse. Pass `--env.scene.num-envs 4096`,
not `--num_envs 4096`, when running under `--framework mjlab`.
- Old log paths (`logs/rsl_rl/`, `logs/skrl/`, `logs/cusrl/`) are no longer
written. New logs are at `logs/isaaclab/<exp>/<run>` and
`logs/mjlab/<exp>/<run>`.

## See also

- README "Frameworks" section.
- `robot_lab.framework` package source for the public adapter API.
Empty file added scripts/__init__.py
Empty file.
Empty file added scripts/isaaclab/__init__.py
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
)

if args_cli.checkpoint is None:
args_cli.checkpoint = os.path.join("logs", "cusrl", agent_cfg.experiment_name)
args_cli.checkpoint = os.path.join("logs", "isaaclab", agent_cfg.experiment_name)
trial = cusrl.Trial(args_cli.checkpoint)
if trial is not None:
log_dir = trial.home
else:
# specify directory for logging videos
log_dir = os.path.join("logs", "cusrl", agent_cfg.experiment_name)
log_dir = os.path.join("logs", "isaaclab", agent_cfg.experiment_name)
log_dir = os.path.abspath(log_dir)

# create isaac environment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
env_cfg.scene.num_envs = int(env_cfg.scene.num_envs / cusrl.utils.distributed.world_size())

# specify directory for logging experiments
log_root_path = os.path.join("logs", "cusrl", agent_cfg.experiment_name)
log_root_path = os.path.join("logs", "isaaclab", agent_cfg.experiment_name)
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Logging experiment in directory: {log_root_path}")
# specify directory for logging runs: {time-stamp}_{run_name}
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
)

# specify directory for logging experiments
log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name)
log_root_path = os.path.join("logs", "isaaclab", agent_cfg.experiment_name)
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Loading experiment from directory: {log_root_path}")
if args_cli.use_pretrained_checkpoint:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
)

# specify directory for logging experiments
log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name)
log_root_path = os.path.join("logs", "isaaclab", agent_cfg.experiment_name)
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Loading experiment from directory: {log_root_path}")
if args_cli.use_pretrained_checkpoint:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
agent_cfg.seed = seed

# specify directory for logging experiments
log_root_path = os.path.join("logs", "rsl_rl", agent_cfg.experiment_name)
log_root_path = os.path.join("logs", "isaaclab", agent_cfg.experiment_name)
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Logging experiment in directory: {log_root_path}")
# specify directory for logging runs: {time-stamp}_{run_name}
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, expe
task_name = args_cli.task.split(":")[-1]

# specify directory for logging experiments (load checkpoint)
log_root_path = os.path.join("logs", "skrl", experiment_cfg["agent"]["experiment"]["directory"])
log_root_path = os.path.join("logs", "isaaclab", experiment_cfg["agent"]["experiment"]["directory"])
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Loading experiment from directory: {log_root_path}")
# get checkpoint path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen
env_cfg.seed = agent_cfg["seed"]

# specify directory for logging experiments
log_root_path = os.path.join("logs", "skrl", agent_cfg["agent"]["experiment"]["directory"])
log_root_path = os.path.join("logs", "isaaclab", agent_cfg["agent"]["experiment"]["directory"])
log_root_path = os.path.abspath(log_root_path)
print(f"[INFO] Logging experiment in directory: {log_root_path}")
# specify directory for logging runs: {time-stamp}_{run_name}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading