Skip to content

feat: dual-framework support (IsaacLab + mjlab)#118

Closed
fan-ziqi wants to merge 3 commits into
mainfrom
devel-mjlab
Closed

feat: dual-framework support (IsaacLab + mjlab)#118
fan-ziqi wants to merge 3 commits into
mainfrom
devel-mjlab

Conversation

@fan-ziqi

Copy link
Copy Markdown
Owner

Summary

Adds dual-backend support: every existing IsaacLab task continues to work, and the project gains a parallel mjlab path through a new adapter layer at robot_lab.framework. Backend is selected at runtime via --framework {isaaclab,mjlab} to a top-level dispatcher; users provision per-framework venvs via ./setup_env.sh.

What's in this PR

3 commits:

  1. feat: dual-framework adapter layer + venv automation

    • robot_lab.framework package: cfg-type aliases, MDP function aliases, sensor / event / scene / contact factories, RL cfg shim, app launcher, register_task, manager_container, make_observation_group — all with parallel implementations on both backends.
    • assets/builders/{isaaclab,mjlab}.py + assets/data/<robot>.py for framework-neutral robot data + per-backend cfg construction.
    • setup_env.sh: provisions .venvs/{isaaclab,mjlab} with fixed Python versions; idempotent. The IsaacLab side drops a .pth so the wheel-bundled isaaclab_rl/_tasks/_assets subpackages become importable without a separate git clone IsaacLab.
    • pyproject.toml: PEP 621 metadata + mutually exclusive [isaaclab] / [mjlab] extras (no [all]: IsaacSim forces py3.11, mjlab is native to py3.12).
  2. refactor: migrate business code to robot_lab.framework adapter

    • robot_lab/__init__.py + tasks/__init__.py: defensive try/ImportError so import robot_lab works under both venvs, even pre-SimApp.
    • assets/<vendor>.py (10 files): wrap IL-only top-level cfg constants in try/ImportError so the modules import cleanly under mjlab.
    • assets/unitree.py::build_unitree_a1_cfg(): factory dispatching on get_framework().
    • velocity_env_cfg.py + mdp/{rewards,events,observations,commands}.py: imports come from robot_lab.framework. Math helpers fall back to mjlab's lab_api. Type-only imports moved into TYPE_CHECKING blocks.
    • 24 velocity task __init__.py files + beyondmimic G1: switched from gym.register to robot_lab.framework.register_task. Task IDs unified to RobotLab-Velocity-...-v0 (Isaac- infix dropped). Every task is framework_required="isaaclab" for now: existing env_cfg subclasses inherit IL-style nested @configclass and don't survive class-body evaluation under mjlab. Authoring mjlab-native env cfgs is a follow-up beyond this PR.
  3. feat: scripts dispatcher + framework tests + docs

    • scripts/{train,play}.py: top-level dispatcher with auto-exec into the right venv.
    • scripts/isaaclab/...: existing scripts moved here. Log root unified to logs/isaaclab/<exp>/<run> (was logs/{rsl_rl,skrl,cusrl}/).
    • scripts/mjlab/reinforcement_learning/rsl_rl/{train,play}.py: thin wrappers over mjlab.scripts.{train,play}. mjlab CLI stays tyro-native (--env.scene.num-envs ...).
    • tests/framework/: detect / spec / container / subprocess-isolated imports / AST-based no-direct-imports audit.
    • docs/migration-from-subclass.md: walks IL users through the small set of changes.
    • README.md: new "Frameworks" section + mjlab column in Version Dependency.

Migration impact for existing IsaacLab users

Two breaking changes:

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

Custom env_cfg subclasses, __post_init__ overrides, and agent cfgs all keep working unchanged. See docs/migration-from-subclass.md for details.

Verification

  • pytest tests/framework/ — 23/23 pass under .venvs/mjlab (subprocess-isolated import smoke + AST audit + container/detect/spec unit tests).
  • pytest tests/framework/test_no_framework_imports.py — 40 files scanned, 0 violations of the "business code does not import isaaclab/mjlab directly" invariant.
  • pytest -m smoke tests/integration/ — mjlab list-tasks pass; IL side currently skipped on this dev host pending an external IsaacSim 5.1 + Python 3.11 SimApp bootstrap fix (verified to be external — a fresh venv with the same code triggers the same kit-bootstrap error).
  • Both venvs cleanly import robot_lab, import robot_lab.framework, and import robot_lab.tasks.
  • All 24 velocity tasks register on IsaacLab (Isaac- infix dropped); under mjlab they are silently skipped with INFO[robot_lab.framework] task ... requires framework='isaaclab'; skipping under current framework. log lines.

Out of scope (follow-ups)

  • Authoring at least one mjlab-native env_cfg factory function (Unitree A1 is the natural template — its asset builder is already in place).
  • Porting the 100+ wheel-bundled IsaacLab subpackage paths to a more robust resolution (current .pth approach works but is wheel-internal).

Why some things look the way they do

  • framework_required="isaaclab" on every task is intentional, not laziness. mjlab's ObservationGroupCfg requires a terms= argument that IsaacLab's doesn't take. The existing nested-@configclass LocomotionVelocityRoughEnvCfg cannot evaluate under mjlab. The skip-with-info pattern is the cleanest way to keep IL behavior identical while leaving the door open for mjlab-native cfgs.
  • configclass is intentionally NOT exposed by robot_lab.framework. It's a dataclass-like decorator on IsaacLab and a no-op identity on mjlab; exposing it would be a footgun (silent semantic divergence). Internal code uses from isaaclab.utils import configclass directly.
  • The [all] extra was deliberately omitted. IsaacSim forces Python 3.11; mjlab is native to 3.12. Combining them in one venv requires picking 3.11 and degrading mjlab. Two-venv discipline is the project's design choice.

fan-ziqi added 3 commits May 30, 2026 10:01
This is the foundation of dual-backend (IsaacLab + mjlab) support.

robot_lab.framework: new package that re-exports cfg types, MDP function
aliases, sensor/event/scene/contact factories, RL cfg shim, app launcher,
and register_task on both backends. Business code imports only from this
namespace; the active backend is selected at first import via the
ROBOT_LAB_FRAMEWORK environment variable.

assets/builders: backend-specific helpers that consume neutral robot data
(URDF/MJCF paths, init pose, actuator wiring) and return either an
isaaclab ArticulationCfg or a mjlab EntityCfg. Per-vendor robot factories
dispatch on get_framework() to pick the right builder.

assets/data: framework-neutral robot constants (Unitree A1 used as the
template; pattern can be replicated per-robot as needed).

pyproject.toml: PEP 621 metadata + two mutually exclusive optional
dependencies, [isaaclab] and [mjlab]. The IsaacSim wheel is routed
through the nvidia index. There is intentionally no [all] extra: IsaacSim
forces Python 3.11 while mjlab is native to 3.12.

setup_env.sh (repo root): provisions per-framework venvs at fixed paths
(.venvs/{isaaclab,mjlab}/) with fixed Python versions. Idempotent. The
IsaacLab side also writes a .pth file so the wheel-bundled isaaclab_rl /
isaaclab_tasks / isaaclab_assets subpackages are importable without
git-cloning IsaacLab.
robot_lab.__init__ + tasks.__init__: defensive try/ImportError around
isaaclab_tasks-dependent imports so the package is importable under the
mjlab venv (where Isaac Sim is absent). IsaacLab behavior is unchanged.

assets/<vendor>.py: 10 vendor files now wrap IL-only top-level constants
(ArticulationCfg, DCMotorCfg, etc.) in try/ImportError. Constants resolve
to None on mjlab; tasks attempting to use them fail at task-instantiation
time rather than crashing module import.

assets/unitree.py: build_unitree_a1_cfg() factory dispatches on
get_framework() and calls the matching builder.

velocity_env_cfg.py: imports come from robot_lab.framework. IL-specific
runtime types (sim_utils, ArticulationCfg, sensor cfgs, terrain helpers)
still import from isaaclab.* but wrapped in try/except. configclass falls
back to a no-op identity on mjlab.

velocity/mdp/*.py: rewards.py, events.py, observations.py, commands.py
swap isaaclab.envs/managers imports for robot_lab.framework re-exports.
Articulation / RigidObject / ContactSensor / RayCaster type imports
moved to TYPE_CHECKING blocks. Math helpers fall back to mjlab.utils.lab_api.

Task __init__.py files (24 velocity quadruped/wheeled/humanoid/others +
beyondmimic): gym.register replaced with framework.register_task. Task IDs
unified to RobotLab-Velocity-... (Isaac- infix dropped). framework_required
="isaaclab" on every task: existing env_cfg classes inherit IL-style
nested @configclass that don't survive class-body evaluation under mjlab.
Authoring mjlab-native env cfgs is a follow-up.

env_cfg passed as "module:Class" string so the cfg module loads lazily —
under mjlab the framework_required gate short-circuits before any IL-only
imports fire.
scripts/{train,play}.py: top-level dispatcher. Parses --framework
{isaaclab,mjlab} (required) and --rl {rsl_rl,skrl,cusrl} (default rsl_rl),
then exec's under .venvs/<framework>/bin/python before delegating to the
backend-specific entry-point. Users can run from system Python without
source-activating any venv.

scripts/{isaaclab,mjlab}/: backend-specific entry points.

  - scripts/isaaclab/reinforcement_learning/{rsl_rl,skrl,cusrl}/{train,play}.py:
    moved from scripts/reinforcement_learning/. Log root unified to
    logs/isaaclab/<exp>/<run> (was logs/{rsl_rl,skrl,cusrl}/<exp>);
    behavior otherwise unchanged.

  - scripts/isaaclab/tools/: convert_urdf, convert_mjcf, list_envs,
    random_agent, zero_agent, train_batch, delivery, clean_trash,
    beyondmimic moved here. cli_trainer remains framework-neutral.

  - scripts/mjlab/reinforcement_learning/rsl_rl/{train,play}.py: thin
    wrappers over mjlab.scripts.{train,play}.main, preceded by
    import robot_lab.tasks so RobotLab-* tasks register into mjlab's
    registry. mjlab uses tyro-based CLI (--env.scene.num-envs ...).

  - scripts/mjlab/tools/list_envs.py: mirrors mjlab.scripts.list_envs.

tests/framework/: subprocess-isolated import smoke + AST-based no-direct-
imports audit (40 files scanned, 0 violations under the agreed scope).

tests/integration/test_smoke.py: opt-in via -m smoke; mjlab side passes;
IL side skipped pending external IsaacSim 5.1 + py3.11 compat fix.

pyproject.toml (root): smoke marker + ROS-plugin block + testpaths.

docs/migration-from-subclass.md: walks IL users through the (small)
changes needed to keep their custom tasks working.

README.md: new Frameworks section + Version Dependency mjlab column.
@fan-ziqi

Copy link
Copy Markdown
Owner Author

Closing — the implementation took the wrong shape. Reopening fresh with a proper IL-named framework layer instead of try/except scaffolding around isaaclab imports. See follow-up PR.

@fan-ziqi fan-ziqi closed this May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant