Instructions to use INSAIT-Institute/LADDER-RoboLab-Long with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use INSAIT-Institute/LADDER-RoboLab-Long with LeRobot:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Download env.py from INSAIT-Institute/LADDER-RoboLab-Long: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/INSAIT-Institute/LADDER-RoboLab-Long/resolve/main/env.py
- Command line
-
hf download hf://INSAIT-Institute/LADDER-RoboLab-Long/env.py
-
curl -L -o env.py https://huggingface.co/INSAIT-Institute/LADDER-RoboLab-Long/resolve/main/env.py
12.2 kB
| # Copyright (c) 2026 INSAIT. MIT License (see LICENSE). | |
| """LADDER RoboLab-Long for LeRobot EnvHub: 44 long-horizon Franka (DROID) tasks on Isaac Sim 6 / Isaac Lab 3. | |
| Each task is one suite of LeRobot's ``{suite: {task_id: env}}``; its 8 episodes run as 8 parallel envs | |
| in one Isaac Lab world. Isaac Sim starts once per process. See README.md. | |
| """ | |
| import os | |
| import re | |
| import sys | |
| REPO_ID = "INSAIT-Institute/LADDER-RoboLab-Long" | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| if not os.path.exists(os.path.join(_HERE, "_ladder_envhub.py")): | |
| # `make_env("<repo>")` downloads only this file: fetch the rest of the same revision. | |
| from huggingface_hub import snapshot_download | |
| _revision = os.path.basename(_HERE) | |
| _HERE = snapshot_download(REPO_ID, revision=_revision if re.fullmatch(r"[0-9a-f]{40}", _revision) else None) | |
| if _HERE not in sys.path: | |
| sys.path.insert(0, _HERE) | |
| import atexit # noqa: E402 | |
| import logging # noqa: E402 | |
| from concurrent.futures import ThreadPoolExecutor # noqa: E402 | |
| from functools import partial # noqa: E402 | |
| from typing import Any # noqa: E402 | |
| import _ladder_envhub as lh # noqa: E402 | |
| EPISODES_PER_TASK = 8 | |
| CONTROL_HZ = 15 | |
| LEROBOT_SIM = lh.lerobot_sim("franka", fps=CONTROL_HZ) | |
| def _add_sources() -> None: | |
| """Isaac Lab, RoboLab (patched to read its assets from ROBOLAB_ASSET_DIR) and openpi_client, as LADDER pins them.""" | |
| if _Isaac.sources: | |
| return | |
| robolab = lh.fetch("robolab_src") | |
| constants = robolab / "robolab" / "constants.py" | |
| text = constants.read_text() | |
| patched = 'ASSET_DIR = os.environ.get("ROBOLAB_ASSET_DIR") or os.path.join(PACKAGE_DIR, "assets")' | |
| if patched not in text: | |
| # LADDER's build_support/python/patches/robolab_constants_asset_dir.patch. | |
| text = text.replace('ASSET_DIR = os.path.join(PACKAGE_DIR, "assets")', patched, 1) | |
| text = text.replace( | |
| " return os.path.join(PACKAGE_DIR, relative_path)", | |
| ' normalized = relative_path.replace(os.sep, "/")\n' | |
| ' if normalized.startswith("assets/"):\n' | |
| ' return os.path.join(ASSET_DIR, *normalized[len("assets/"):].split("/"))\n' | |
| " return os.path.join(PACKAGE_DIR, relative_path)", | |
| 1, | |
| ) | |
| # Atomically: another process may be importing it. | |
| staged = constants.with_name(f".constants.{os.getpid()}.py") | |
| staged.write_text(text) | |
| os.replace(staged, constants) | |
| if patched not in constants.read_text(): | |
| raise RuntimeError(f"{constants}: LADDER's asset-directory patch no longer applies") | |
| # The extracted wheel is an installed package on sys.path, with its dist-info (see _ladder_envhub.DOWNLOADS). | |
| lh.add_to_path(lh.fetch("isaaclab"), robolab, lh.fetch("openpi_src") / "packages" / "openpi-client" / "src") | |
| os.environ.setdefault("ROBOLAB_ASSET_DIR", str(lh.fetch("robolab_assets") / "assets")) | |
| _Isaac.sources = True | |
| def task_names() -> list[str]: | |
| """The suite's task names, from LADDER's own registry.""" | |
| from ladder.components.inference.robots.robolab.composition.specs import SPECS | |
| return sorted(SPECS) | |
| class _Isaac: | |
| """Isaac Sim starts once per process and runs on one thread; one task's world is kept at a time.""" | |
| thread = ThreadPoolExecutor(max_workers=1, thread_name_prefix="ladder-isaac") | |
| sources = False | |
| app: Any = None | |
| env: Any = None | |
| env_cfg: Any = None | |
| oracle: Any = None | |
| key: Any = None | |
| def _start_isaac() -> None: | |
| if _Isaac.app is not None: | |
| return | |
| _add_sources() | |
| # Running this environment accepts the NVIDIA Isaac Sim EULA (see README); Kit asks on stdin otherwise. | |
| os.environ.setdefault("OMNI_KIT_ACCEPT_EULA", "YES") | |
| os.environ.setdefault("ACCEPT_EULA", "Y") | |
| import cv2 # noqa: F401 | |
| from isaaclab.app import AppLauncher | |
| root = logging.getLogger() | |
| levels = {handler: handler.level for handler in root.handlers} | |
| with lh.ignore_signal_in_thread(): | |
| _Isaac.app = AppLauncher(headless=True, enable_cameras=True).app | |
| for handler, level in levels.items(): # Kit lowers the levels of existing handlers | |
| handler.setLevel(level) | |
| # Kit hangs in its own shutdown; leave without it, keeping the exit status. | |
| atexit.register( | |
| lambda: ( | |
| sys.stdout.flush(), | |
| sys.stderr.flush(), | |
| os._exit(0 if getattr(sys, "last_value", None) is None else 1), | |
| ) | |
| ) | |
| import carb.settings | |
| import robolab.constants | |
| # As LADDER's env_server.py: LADDER scores subgoals itself, and renderer failures stay visible. | |
| robolab.constants.ENABLE_SUBTASK_PROGRESS_CHECKING = False | |
| carb.settings.get_settings().set("/persistent/app/usd/muteUsdDiagnostics", False) | |
| class RoboLabLong(lh.LadderVectorEnv): | |
| suite = "robolab" | |
| robot = "franka" | |
| default_image_size = (224, 224) | |
| episodes_per_task = EPISODES_PER_TASK | |
| fps = CONTROL_HZ | |
| def task_budget(cls, task: str) -> int: | |
| from ladder.components.inference.robots.robolab.composition.specs import SPECS | |
| return int(SPECS[task].episode_length_s * CONTROL_HZ) | |
| def __init__(self, *args: Any, **kwargs: Any) -> None: | |
| super().__init__(*args, **kwargs) | |
| self._env = None | |
| self._raw = None | |
| self._converter = None | |
| # LeRobot's server builds an env (one env of the first task) before it reports ready. Isaac Sim starts here, | |
| # and the first env of a process builds its world here too: that compiles the shaders and loads the assets | |
| # every task shares, which on a new machine or container would otherwise fall in a client's first open. | |
| # A client's own batch size gets its world on its first reset. | |
| self._fresh = _Isaac.thread.submit(self._start).result() | |
| def _start(self) -> bool: | |
| _start_isaac() | |
| return _Isaac.env is None and self._build() | |
| def goal_sources(self) -> list[str | None] | None: | |
| # The object each goal places (a relay's legs are separate goals), as LADDER's privileged.py names it. | |
| return [ | |
| next((geometry[0] for _, geometry in reversed(list(goal.legs)) if geometry is not None), None) | |
| for goal in _Isaac.oracle or [] | |
| ] | |
| def _backend_reset(self, episode_ids: list[int]) -> list[Any]: | |
| return _Isaac.thread.submit(self._reset).result() | |
| def _backend_step(self, actions: list[Any]) -> list[Any]: | |
| if _Isaac.env is not self._env: | |
| raise RuntimeError("Another task's world replaced this one: run one task at a time (--env.max_parallel_tasks=1)") | |
| return _Isaac.thread.submit(self._step, actions).result() | |
| def _build(self) -> bool: | |
| """Build this task's world at this batch size, unless the process holds it; True if it was built.""" | |
| from ladder.components.inference.robots.robolab.composition.specs import task_class_name, task_target | |
| from ladder.components.inference.robots.robolab.oracle import build_oracle | |
| from robolab.core.environments.runtime import create_env | |
| from robolab.registrations.droid.auto_env_registrations_jointpos import auto_register_droid_envs | |
| self._class_name = task_class_name(self.task) | |
| key = (self._class_name, self.num_envs) | |
| if _Isaac.key == key: | |
| return False | |
| if _Isaac.env is not None: | |
| _Isaac.env.close() | |
| _Isaac.env = _Isaac.key = None | |
| auto_register_droid_envs(task_dirs=None, task=[task_target(self.task)], cameras=None) | |
| # The defaults of LADDER's env_server.py (RoboLab's add_common_eval_args). | |
| _Isaac.env, _Isaac.env_cfg = create_env( | |
| self._class_name, | |
| device="cuda:0", | |
| num_envs=self.num_envs, | |
| instruction_type="default", | |
| policy="ladder_runner", | |
| renderer="realtime", | |
| rendering_mode="balanced", | |
| ) | |
| _Isaac.oracle = build_oracle(getattr(_Isaac.env.cfg, "subtasks", None), _Isaac.env_cfg.instruction) | |
| _Isaac.key = key | |
| if int(_Isaac.env.max_episode_length) != self._max_episode_steps: | |
| logging.getLogger(__name__).warning( | |
| "%s: Isaac Lab budget %d steps, expected %d", self.task, _Isaac.env.max_episode_length, | |
| self._max_episode_steps, | |
| ) | |
| return True | |
| def _reset(self) -> list[Any]: | |
| from ladder.components.inference.robots.robolab.droid_conversions import DroidJointposConverter | |
| # As LADDER's env_server.py: a new world is reset as it is, a kept one first unfreezes its finished envs. | |
| if not self._build() and not self._fresh: | |
| _Isaac.env.reset_eval_state() | |
| self._fresh = False | |
| self._env = _Isaac.env | |
| self._env.reset() | |
| self._raw, _ = self._env.reset() # RoboLab's own double reset (robolab.eval.episode.run_episode) | |
| # DroidJointposConverter takes (width, height), as LADDER's --image-size. | |
| self._converter = DroidJointposConverter(num_envs=self.num_envs, image_size=(self._w, self._h)) | |
| return self._observations() | |
| def _step(self, actions: list[Any]) -> list[Any]: | |
| import omni.kit.app | |
| import omni.timeline | |
| import torch | |
| joints = self._raw["proprio_obs"]["arm_joint_pos"].detach().cpu().numpy() | |
| commands = self._converter.actions_to_jointpos(actions, joints) | |
| timeline, kit = omni.timeline.get_timeline_interface(), omni.kit.app.get_app() | |
| while not timeline.is_playing(): | |
| kit.update() | |
| self._raw, _, _, _, _ = self._env.step(torch.as_tensor(commands, device=self._env.device)) | |
| return self._observations() | |
| def _observations(self) -> list[Any]: | |
| images, proprio = self._raw["image_obs"], self._raw["proprio_obs"] | |
| results = self._env.get_env_results() | |
| active = set(self._env.active_env_ids) | |
| # During a step, _step_count is not yet incremented; LADDER's server counts the step first. | |
| out_of_time = self._step_count + 1 >= int(self._env.max_episode_length) | |
| achieved = None | |
| if _Isaac.oracle: | |
| achieved = [[bool(g.check(self._env, env_id=i)) for g in _Isaac.oracle] for i in range(self.num_envs)] | |
| observations = [] | |
| for i in range(self.num_envs): | |
| success = bool(results[i]["success"]) | |
| metadata = { | |
| "env_name": self._class_name, | |
| "robot_name": "franka", | |
| "instruction": _Isaac.env_cfg.instruction, | |
| "done": success, | |
| "truncated": out_of_time or i not in active, | |
| "oracle": {"achieved": achieved[i] if achieved is not None else [success]}, | |
| } | |
| observations.append( | |
| self._converter.observation_dict( | |
| env_i=i, | |
| main_image=images["over_shoulder_left_camera"][i].detach().cpu().numpy(), | |
| wrist_image=images["wrist_cam"][i].detach().cpu().numpy(), | |
| joints=proprio["arm_joint_pos"][i].detach().cpu().numpy(), | |
| gripper_position=proprio["gripper_pos"][i].detach().cpu().numpy(), | |
| metadata=metadata, | |
| ) | |
| ) | |
| return observations | |
| def vector_env_fns( | |
| task: str = "", | |
| n_envs: int = EPISODES_PER_TASK, | |
| **options: Any, | |
| ) -> dict[str, dict[int, Any]]: | |
| """One factory per task, ``{task: {0: factory}}``: every task, or the comma-separated ``task``. | |
| LeRobot builds only the tasks it runs (``--env.task``), so listing all 44 costs nothing. | |
| """ | |
| names = lh.select_tasks(task, task_names()) | |
| _add_sources() # download before the server reports ready | |
| return {name: {0: partial(RoboLabLong, name, n_envs, **options)} for name in names} | |
| def make_env(n_envs: int = EPISODES_PER_TASK, use_async_envs: bool = False, task: str = "", **options: Any): | |
| """One task as a batched vector env (``--env.use_native_batch``); LeRobot otherwise uses ``vector_env_fns``.""" | |
| name = lh.one_task(task, task_names()) | |
| return {name: {0: vector_env_fns(name, n_envs, **options)[name][0]()}} | |