From 71d57bcef822ab35535eaef5e3c32cf1143d27bc Mon Sep 17 00:00:00 2001 From: klakhi Date: Wed, 20 Aug 2025 05:46:49 +0000 Subject: [PATCH 01/50] Template generator non interactive mode --- CONTRIBUTORS.md | 1 + isaaclab.bat | 24 +++++- tools/template/non_interactive.py | 120 ++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 tools/template/non_interactive.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 3c683ebe4f0..6421d3512fa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -82,6 +82,7 @@ Guidelines for modifications: * Johnson Sun * Kaixi Bao * Kris Wilson +* Krishna Lakhi * Kourosh Darvish * Kousheek Chakraborty * Lionel Gulich diff --git a/isaaclab.bat b/isaaclab.bat index d4862217f34..5d5e7ad912b 100644 --- a/isaaclab.bat +++ b/isaaclab.bat @@ -567,11 +567,15 @@ if "%arg%"=="-i" ( ) else if "%arg%"=="-n" ( rem run the template generator script call :extract_python_exe + rem detect non-interactive flag while reconstructing arguments + set "isNonInteractive=0" set "allArgs=" + set "skip=" for %%a in (%*) do ( REM Append each argument to the variable, skip the first one if defined skip ( - set "allArgs=!allArgs! %%a" + if /I "%%~a"=="--non-interactive" set "isNonInteractive=1" + set "allArgs=!allArgs! ^"%%~a^"" ) else ( set "skip=1" ) @@ -581,16 +585,24 @@ if "%arg%"=="-i" ( echo. echo [INFO] Running template generator... echo. - call !python_exe! tools\template\cli.py !allArgs! + if "!isNonInteractive!"=="1" ( + call !python_exe! tools\template\non_interactive.py !allArgs! + ) else ( + call !python_exe! tools\template\cli.py !allArgs! + ) goto :end ) else if "%arg%"=="--new" ( rem run the template generator script call :extract_python_exe + rem detect non-interactive flag while reconstructing arguments + set "isNonInteractive=0" set "allArgs=" + set "skip=" for %%a in (%*) do ( REM Append each argument to the variable, skip the first one if defined skip ( - set "allArgs=!allArgs! %%a" + if /I "%%~a"=="--non-interactive" set "isNonInteractive=1" + set "allArgs=!allArgs! ^"%%~a^"" ) else ( set "skip=1" ) @@ -600,7 +612,11 @@ if "%arg%"=="-i" ( echo. echo [INFO] Running template generator... echo. - call !python_exe! tools\template\cli.py !allArgs! + if "!isNonInteractive!"=="1" ( + call !python_exe! tools\template\non_interactive.py !allArgs! + ) else ( + call !python_exe! tools\template\cli.py !allArgs! + ) goto :end ) else if "%arg%"=="-t" ( rem run the python provided by Isaac Sim diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py new file mode 100644 index 00000000000..4eb0fd14edc --- /dev/null +++ b/tools/template/non_interactive.py @@ -0,0 +1,120 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import argparse +import os +from typing import List, Dict + +from common import ROOT_DIR +from generator import generate, get_algorithms_per_rl_library + + +def _parse_workflow_arg(item: str) -> Dict[str, str]: + raw = item.strip().lower() + # Enforce strict underscore format: "_" + if "_" not in raw or any(sep in raw for sep in ("|", ":", " ")): + raise ValueError( + "Invalid workflow format. Use underscore format like 'direct_single_agent' or 'manager-based_single_agent'" + ) + name_token, type_token_raw = raw.split("_", 1) + type_token = type_token_raw.replace("_", "-") # normalize to single-agent / multi-agent + + if name_token not in {"direct", "manager-based"}: + raise ValueError( + f"Invalid workflow name: {name_token}. Allowed: 'direct' or 'manager-based'" + ) + if type_token not in {"single-agent", "multi-agent"}: + raise ValueError( + f"Invalid workflow type: {type_token}. Allowed: 'single-agent' or 'multi-agent'" + ) + + return {"name": name_token, "type": type_token} + + +def _validate_external_path(path: str) -> None: + if os.path.abspath(path).startswith(os.path.abspath(ROOT_DIR)): + raise ValueError("External project path cannot be within the Isaac Lab project") + + +def main(argv: List[str] | None = None) -> None: + ''' + Non-interactive entrypoint for the template generator workflow. + + Parses command-line flags, builds the specification dict, and calls generate(). + This avoids any interactive prompts or dependencies on Inquirer-based flow. + ''' + + parser = argparse.ArgumentParser(add_help=False) + supported_workflows = [ + "direct_single_agent", + "direct_multi_agent", + "manager-based_single_agent", + ] + supported_rl_libraries = ["rl_games", "rsl_rl", "skrl", "sb3"] + # All known algorithms across libraries (lowercase for consistent CLI input) + _all_algos_map = get_algorithms_per_rl_library(True, True) + rl_algo_choices = sorted( + {algo.lower() for algos in _all_algos_map.values() for algo in algos} + ) + + parser.add_argument("--task-type", "--task_type", type=str, required=True, choices=["External", "Internal"]) + parser.add_argument("--project-path", "--project_path", type=str) + parser.add_argument("--project-name", "--project_name", type=str, required=True) + parser.add_argument( + "--workflow", + action="append", + required=True, + type=str.lower, + choices=[w.lower() for w in supported_workflows], + ) + parser.add_argument("--rl-library", "--rl_library", type=str.lower, required=True, choices=supported_rl_libraries) + parser.add_argument("--rl-algorithm", "--rl_algorithm", type=str.lower, required=True, choices=rl_algo_choices) + + args, _ = parser.parse_known_args(argv) + + is_external = args.task_type.lower() == "external" + if is_external: + if not args.project_path: + raise ValueError("--project-path is required for External task type") + _validate_external_path(args.project_path) + project_path = args.project_path + else: + project_path = None + + if not args.project_name.isidentifier(): + raise ValueError("--project-name must be a valid identifier (letters, numbers, underscores)") + + workflows = [_parse_workflow_arg(item) for item in args.workflow] + single_agent = any(wf["type"] == "single-agent" for wf in workflows) + multi_agent = any(wf["type"] == "multi-agent" for wf in workflows) + + # Filter allowed algorithms per RL library under given workflow capabilities + algos_map = get_algorithms_per_rl_library(single_agent, multi_agent) + lib = args.rl_library.strip().lower() + algo = args.rl_algorithm.strip().lower() + supported_algos = [a.lower() for a in algos_map.get(lib, [])] + if algo not in supported_algos: + allowed = ", ".join(supported_algos) if supported_algos else "none" + raise ValueError( + f"Algorithm '{args.rl_algorithm}' is not supported for {lib} under selected workflows. Allowed: {allowed}" + ) + + specification = { + "external": is_external, + "path": project_path, + "name": args.project_name, + "workflows": workflows, + "rl_libraries": [{"name": lib, "algorithms": [algo]}], + } + + generate(specification) + + +if __name__ == "__main__": + main() + + From 403792fefb7cbf63ee915061e269ce2ed4316b3f Mon Sep 17 00:00:00 2001 From: klakhi Date: Wed, 20 Aug 2025 07:12:42 +0000 Subject: [PATCH 02/50] Extend non-interactive support to linux as well --- isaaclab.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isaaclab.sh b/isaaclab.sh index fed536e680a..e5288f7bed1 100755 --- a/isaaclab.sh +++ b/isaaclab.sh @@ -486,7 +486,11 @@ while [[ $# -gt 0 ]]; do echo "[INFO] Installing template dependencies..." ${python_exe} -m pip install -q -r ${ISAACLAB_PATH}/tools/template/requirements.txt echo -e "\n[INFO] Running template generator...\n" - ${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py $@ + if [[ " $* " == *" --non-interactive "* ]]; then + ${python_exe} ${ISAACLAB_PATH}/tools/template/non_interactive.py "$@" + else + ${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py $@ + fi # exit neatly break ;; From c1b8cb7c52c75f43b4321407a06ae40e5507c812 Mon Sep 17 00:00:00 2001 From: klakhi Date: Wed, 20 Aug 2025 07:38:18 +0000 Subject: [PATCH 03/50] fixing some format flagged issues --- tools/template/non_interactive.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py index 4eb0fd14edc..fbe9185ac40 100644 --- a/tools/template/non_interactive.py +++ b/tools/template/non_interactive.py @@ -7,13 +7,12 @@ import argparse import os -from typing import List, Dict from common import ROOT_DIR from generator import generate, get_algorithms_per_rl_library -def _parse_workflow_arg(item: str) -> Dict[str, str]: +def _parse_workflow_arg(item: str) -> dict[str, str]: raw = item.strip().lower() # Enforce strict underscore format: "_" if "_" not in raw or any(sep in raw for sep in ("|", ":", " ")): @@ -24,13 +23,9 @@ def _parse_workflow_arg(item: str) -> Dict[str, str]: type_token = type_token_raw.replace("_", "-") # normalize to single-agent / multi-agent if name_token not in {"direct", "manager-based"}: - raise ValueError( - f"Invalid workflow name: {name_token}. Allowed: 'direct' or 'manager-based'" - ) + raise ValueError(f"Invalid workflow name: {name_token}. Allowed: 'direct' or 'manager-based'") if type_token not in {"single-agent", "multi-agent"}: - raise ValueError( - f"Invalid workflow type: {type_token}. Allowed: 'single-agent' or 'multi-agent'" - ) + raise ValueError(f"Invalid workflow type: {type_token}. Allowed: 'single-agent' or 'multi-agent'") return {"name": name_token, "type": type_token} @@ -40,14 +35,14 @@ def _validate_external_path(path: str) -> None: raise ValueError("External project path cannot be within the Isaac Lab project") -def main(argv: List[str] | None = None) -> None: - ''' +def main(argv: list[str] | None = None) -> None: + """ Non-interactive entrypoint for the template generator workflow. Parses command-line flags, builds the specification dict, and calls generate(). This avoids any interactive prompts or dependencies on Inquirer-based flow. - ''' - + """ + parser = argparse.ArgumentParser(add_help=False) supported_workflows = [ "direct_single_agent", @@ -57,9 +52,7 @@ def main(argv: List[str] | None = None) -> None: supported_rl_libraries = ["rl_games", "rsl_rl", "skrl", "sb3"] # All known algorithms across libraries (lowercase for consistent CLI input) _all_algos_map = get_algorithms_per_rl_library(True, True) - rl_algo_choices = sorted( - {algo.lower() for algos in _all_algos_map.values() for algo in algos} - ) + rl_algo_choices = sorted({algo.lower() for algos in _all_algos_map.values() for algo in algos}) parser.add_argument("--task-type", "--task_type", type=str, required=True, choices=["External", "Internal"]) parser.add_argument("--project-path", "--project_path", type=str) @@ -116,5 +109,3 @@ def main(argv: List[str] | None = None) -> None: if __name__ == "__main__": main() - - From 1787e18eefe4814edc18faafcbd6fa4879f20e6f Mon Sep 17 00:00:00 2001 From: klakhi Date: Fri, 22 Aug 2025 07:09:17 +0000 Subject: [PATCH 04/50] handle scenario where no rl-algorithm is supplied i.e auto select --- tools/template/non_interactive.py | 42 +++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py index fbe9185ac40..9d81087c787 100644 --- a/tools/template/non_interactive.py +++ b/tools/template/non_interactive.py @@ -65,7 +65,18 @@ def main(argv: list[str] | None = None) -> None: choices=[w.lower() for w in supported_workflows], ) parser.add_argument("--rl-library", "--rl_library", type=str.lower, required=True, choices=supported_rl_libraries) - parser.add_argument("--rl-algorithm", "--rl_algorithm", type=str.lower, required=True, choices=rl_algo_choices) + parser.add_argument( + "--rl-algorithm", + "--rl_algorithm", + type=str.lower, + required=False, + default=None, + choices=rl_algo_choices, + help=( + "RL algorithm to use. If omitted, the tool auto-selects when exactly one algorithm " + "is valid for the chosen workflows and library." + ), + ) args, _ = parser.parse_known_args(argv) @@ -88,13 +99,30 @@ def main(argv: list[str] | None = None) -> None: # Filter allowed algorithms per RL library under given workflow capabilities algos_map = get_algorithms_per_rl_library(single_agent, multi_agent) lib = args.rl_library.strip().lower() - algo = args.rl_algorithm.strip().lower() supported_algos = [a.lower() for a in algos_map.get(lib, [])] - if algo not in supported_algos: - allowed = ", ".join(supported_algos) if supported_algos else "none" - raise ValueError( - f"Algorithm '{args.rl_algorithm}' is not supported for {lib} under selected workflows. Allowed: {allowed}" - ) + + # Auto-select algorithm if not provided + if args.rl_algorithm is None: + if len(supported_algos) == 0: + raise ValueError( + f"No algorithms are supported for {lib} under the selected workflows. " + "Please choose a different combination." + ) + if len(supported_algos) > 1: + allowed = ", ".join(supported_algos) + raise ValueError( + "Multiple algorithms are valid for the selected workflows and library. " + f"Please specify one using --rl-algorithm. Allowed: {allowed}" + ) + algo = supported_algos[0] + else: + algo = args.rl_algorithm.strip().lower() + if algo not in supported_algos: + allowed = ", ".join(supported_algos) if supported_algos else "none" + raise ValueError( + f"Algorithm '{args.rl_algorithm}' is not supported for {lib} under selected workflows. Allowed:" + f" {allowed}" + ) specification = { "external": is_external, From 5c5020859ae2eafa7761e28c1870109d52833f0f Mon Sep 17 00:00:00 2001 From: klakhi Date: Fri, 22 Aug 2025 11:57:13 +0000 Subject: [PATCH 05/50] support all based scenario --- tools/template/non_interactive.py | 105 +++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py index 9d81087c787..e039b4c06d6 100644 --- a/tools/template/non_interactive.py +++ b/tools/template/non_interactive.py @@ -62,16 +62,22 @@ def main(argv: list[str] | None = None) -> None: action="append", required=True, type=str.lower, - choices=[w.lower() for w in supported_workflows], + choices=[*([w.lower() for w in supported_workflows]), "all"], + ) + parser.add_argument( + "--rl-library", + "--rl_library", + type=str.lower, + required=True, + choices=[*supported_rl_libraries, "all"], ) - parser.add_argument("--rl-library", "--rl_library", type=str.lower, required=True, choices=supported_rl_libraries) parser.add_argument( "--rl-algorithm", "--rl_algorithm", type=str.lower, required=False, default=None, - choices=rl_algo_choices, + choices=[*rl_algo_choices, "all"], help=( "RL algorithm to use. If omitted, the tool auto-selects when exactly one algorithm " "is valid for the chosen workflows and library." @@ -92,36 +98,87 @@ def main(argv: list[str] | None = None) -> None: if not args.project_name.isidentifier(): raise ValueError("--project-name must be a valid identifier (letters, numbers, underscores)") - workflows = [_parse_workflow_arg(item) for item in args.workflow] + # Expand workflows: allow "all" to mean all supported workflows + if any(item == "all" for item in args.workflow): + workflows = [_parse_workflow_arg(item) for item in supported_workflows] + else: + workflows = [_parse_workflow_arg(item) for item in args.workflow] single_agent = any(wf["type"] == "single-agent" for wf in workflows) multi_agent = any(wf["type"] == "multi-agent" for wf in workflows) # Filter allowed algorithms per RL library under given workflow capabilities algos_map = get_algorithms_per_rl_library(single_agent, multi_agent) - lib = args.rl_library.strip().lower() - supported_algos = [a.lower() for a in algos_map.get(lib, [])] - # Auto-select algorithm if not provided - if args.rl_algorithm is None: - if len(supported_algos) == 0: + # Expand RL libraries: allow "all" to mean all libraries that have at least one supported algorithm + rl_lib_input = args.rl_library.strip().lower() + if rl_lib_input == "all": + selected_libs = [lib for lib, algos in algos_map.items() if len(algos) > 0] + if not selected_libs: raise ValueError( - f"No algorithms are supported for {lib} under the selected workflows. " - "Please choose a different combination." + "No RL libraries are supported under the selected workflows. Please choose different workflows." ) - if len(supported_algos) > 1: - allowed = ", ".join(supported_algos) - raise ValueError( - "Multiple algorithms are valid for the selected workflows and library. " - f"Please specify one using --rl-algorithm. Allowed: {allowed}" - ) - algo = supported_algos[0] else: - algo = args.rl_algorithm.strip().lower() - if algo not in supported_algos: - allowed = ", ".join(supported_algos) if supported_algos else "none" + selected_libs = [rl_lib_input] + if rl_lib_input not in algos_map: + raise ValueError(f"Unknown RL library: {rl_lib_input}") + # Pre-compute supported algorithms per selected library (lowercased) + supported_algos_per_lib = {lib: [a.lower() for a in algos_map.get(lib, [])] for lib in selected_libs} + + # Auto-select algorithm if not provided + rl_algo_input = args.rl_algorithm.strip().lower() if args.rl_algorithm is not None else None + + rl_libraries_spec = [] + if rl_algo_input is None: + # If a single library is selected, preserve previous behavior + if len(selected_libs) == 1: + lib = selected_libs[0] + supported_algos = supported_algos_per_lib.get(lib, []) + if len(supported_algos) == 0: + raise ValueError( + f"No algorithms are supported for {lib} under the selected workflows. " + "Please choose a different combination." + ) + if len(supported_algos) > 1: + allowed = ", ".join(supported_algos) + raise ValueError( + "Multiple algorithms are valid for the selected workflows and library. " + f"Please specify one using --rl-algorithm or use --rl-algorithm all. Allowed: {allowed}" + ) + rl_libraries_spec.append({"name": lib, "algorithms": [supported_algos[0]]}) + else: + # Multiple libraries selected. If each has exactly one algorithm, auto-select; otherwise require explicit choice. + libs_with_multi = [lib for lib, algos in supported_algos_per_lib.items() if len(algos) > 1] + if libs_with_multi: + details = "; ".join(f"{lib}: {', '.join(supported_algos_per_lib[lib])}" for lib in libs_with_multi) + raise ValueError( + "Multiple algorithms are valid for one or more libraries under the selected workflows. " + "Please specify --rl-algorithm or use --rl-algorithm all. Details: " + + details + ) + for lib, algos in supported_algos_per_lib.items(): + if not algos: + continue + rl_libraries_spec.append({"name": lib, "algorithms": [algos[0]]}) + elif rl_algo_input == "all": + # Include all supported algorithms per selected library + for lib, algos in supported_algos_per_lib.items(): + if not algos: + continue + rl_libraries_spec.append({"name": lib, "algorithms": algos}) + if not rl_libraries_spec: + raise ValueError("No algorithms are supported under the selected workflows.") + else: + # Specific algorithm requested: include only libraries that support it + matching_libs = [] + for lib, algos in supported_algos_per_lib.items(): + if rl_algo_input in algos: + matching_libs.append(lib) + rl_libraries_spec.append({"name": lib, "algorithms": [rl_algo_input]}) + if not matching_libs: + allowed_desc = {lib: algos for lib, algos in supported_algos_per_lib.items() if algos} raise ValueError( - f"Algorithm '{args.rl_algorithm}' is not supported for {lib} under selected workflows. Allowed:" - f" {allowed}" + f"Algorithm '{args.rl_algorithm}' is not supported under the selected workflows for the chosen" + f" libraries. Supported per library: {allowed_desc}" ) specification = { @@ -129,7 +186,7 @@ def main(argv: list[str] | None = None) -> None: "path": project_path, "name": args.project_name, "workflows": workflows, - "rl_libraries": [{"name": lib, "algorithms": [algo]}], + "rl_libraries": rl_libraries_spec, } generate(specification) From 69f341213dc2d5a4c5e3e1ac9440bd8132985c62 Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Mon, 8 Sep 2025 22:11:54 -0700 Subject: [PATCH 06/50] Adds a unit tests for catching non-headless app file launch (#3392) # Description Recent isaac sim update introduced a new bug for non-headless scripts where some scripts were hanging at simulation startup. This change introduces a new unit test that aims to capture issues like this by forcing the use of the non-headless app file. Additionally, the isaac sim CI system has very unstable results for perf testing, so we are disabling the performance-related tests for the sim CI. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../test/app/test_non_headless_launch.py | 65 +++++++++++++++++++ .../test_kit_startup_performance.py | 3 - .../test_robot_load_performance.py | 1 - 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 source/isaaclab/test/app/test_non_headless_launch.py diff --git a/source/isaaclab/test/app/test_non_headless_launch.py b/source/isaaclab/test/app/test_non_headless_launch.py new file mode 100644 index 00000000000..52c35a10916 --- /dev/null +++ b/source/isaaclab/test/app/test_non_headless_launch.py @@ -0,0 +1,65 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script checks if the app can be launched with non-headless app and start the simulation. +""" + +"""Launch Isaac Sim Simulator first.""" + + +import pytest + +from isaaclab.app import AppLauncher + +# launch omniverse app +app_launcher = AppLauncher(experience="isaaclab.python.kit", headless=True) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.utils import configclass + + +@configclass +class SensorsSceneCfg(InteractiveSceneCfg): + """Design the scene with sensors on the robot.""" + + # ground plane + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + +def run_simulator( + sim: sim_utils.SimulationContext, +): + """Run the simulator.""" + + count = 0 + + # Simulate physics + while simulation_app.is_running() and count < 100: + # perform step + sim.step() + count += 1 + + +@pytest.mark.isaacsim_ci +def test_non_headless_launch(): + # Initialize the simulation context + sim_cfg = sim_utils.SimulationCfg(dt=0.005) + sim = sim_utils.SimulationContext(sim_cfg) + # design scene + scene_cfg = SensorsSceneCfg(num_envs=1, env_spacing=2.0) + scene = InteractiveScene(scene_cfg) + print(scene) + # Play the simulator + sim.reset() + # Now we are ready! + print("[INFO]: Setup complete...") + # Run the simulator + run_simulator(sim) diff --git a/source/isaaclab/test/performance/test_kit_startup_performance.py b/source/isaaclab/test/performance/test_kit_startup_performance.py index 056b2e6293b..dfa716cd0b2 100644 --- a/source/isaaclab/test/performance/test_kit_startup_performance.py +++ b/source/isaaclab/test/performance/test_kit_startup_performance.py @@ -10,12 +10,9 @@ import time -import pytest - from isaaclab.app import AppLauncher -@pytest.mark.isaacsim_ci def test_kit_start_up_time(): """Test kit start-up time.""" start_time = time.time() diff --git a/source/isaaclab/test/performance/test_robot_load_performance.py b/source/isaaclab/test/performance/test_robot_load_performance.py index 4acf8ad6331..bca8c36d9d5 100644 --- a/source/isaaclab/test/performance/test_robot_load_performance.py +++ b/source/isaaclab/test/performance/test_robot_load_performance.py @@ -33,7 +33,6 @@ ({"name": "Anymal_D", "robot_cfg": ANYMAL_D_CFG, "expected_load_time": 40.0}, "cpu"), ], ) -@pytest.mark.isaacsim_ci def test_robot_load_performance(test_config, device): """Test robot load time.""" with build_simulation_context(device=device) as sim: From 7ee6d2a7b7d3fb41e5c3d635dacafc3eb037d20c Mon Sep 17 00:00:00 2001 From: Philipp Reist <66367163+preist-nvidia@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:58:55 +0200 Subject: [PATCH 07/50] Clarifies asset classes' default_inertia tensor coordinate frame (#3405) # Description The default_inertia attributes of the Articulation, RigidObjectCollection, and RigidObject data asset classes did not specify in what coordinate frame the tensors should be provided. This PR addresses this, and addresses some minor inconsistencies across the default_inertia docstrings. ## Type of change - This change requires a documentation update ## Screenshots ArticulationData | Before | After | | ------ | ----- | | image| image| RigidObjectCollectionData | Before | After | | ------ | ----- | | image | image | RigidObjectData | Before | After | | ------ | ----- | | image | image | ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- CONTRIBUTORS.md | 1 + .../isaaclab/assets/articulation/articulation_data.py | 5 +++-- .../isaaclab/assets/rigid_object/rigid_object_data.py | 7 +++++-- .../rigid_object_collection_data.py | 7 +++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ee6200de869..47335ecb0fb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -111,6 +111,7 @@ Guidelines for modifications: * Özhan Özen * Patrick Yin * Peter Du +* Philipp Reist * Pulkit Goyal * Qian Wan * Qinxi Yu diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation_data.py b/source/isaaclab/isaaclab/assets/articulation/articulation_data.py index 145a69dfc85..99b2f76abfa 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation_data.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation_data.py @@ -151,8 +151,9 @@ def update(self, dt: float): default_inertia: torch.Tensor = None """Default inertia for all the bodies in the articulation. Shape is (num_instances, num_bodies, 9). - The inertia is the inertia tensor relative to the center of mass frame. The values are stored in - the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + The inertia tensor should be given with respect to the center of mass, expressed in the articulation links' actor frame. + The values are stored in the order :math:`[I_{xx}, I_{yx}, I_{zx}, I_{xy}, I_{yy}, I_{zy}, I_{xz}, I_{yz}, I_{zz}]`. + However, due to the symmetry of inertia tensors, row- and column-major orders are equivalent. This quantity is parsed from the USD schema at the time of initialization. """ diff --git a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py index 3aac87d324f..ee83900376f 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object_data.py @@ -112,8 +112,11 @@ def update(self, dt: float): default_inertia: torch.Tensor = None """Default inertia tensor read from the simulation. Shape is (num_instances, 9). - The inertia is the inertia tensor relative to the center of mass frame. The values are stored in - the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + The inertia tensor should be given with respect to the center of mass, expressed in the rigid body's actor frame. + The values are stored in the order :math:`[I_{xx}, I_{yx}, I_{zx}, I_{xy}, I_{yy}, I_{zy}, I_{xz}, I_{yz}, I_{zz}]`. + However, due to the symmetry of inertia tensors, row- and column-major orders are equivalent. + + This quantity is parsed from the USD schema at the time of initialization. """ ## diff --git a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py index 897679f75aa..328010bb14f 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection_data.py @@ -118,8 +118,11 @@ def update(self, dt: float): default_inertia: torch.Tensor = None """Default object inertia tensor read from the simulation. Shape is (num_instances, num_objects, 9). - The inertia is the inertia tensor relative to the center of mass frame. The values are stored in - the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + The inertia tensor should be given with respect to the center of mass, expressed in the rigid body's actor frame. + The values are stored in the order :math:`[I_{xx}, I_{yx}, I_{zx}, I_{xy}, I_{yy}, I_{zy}, I_{xz}, I_{yz}, I_{zz}]`. + However, due to the symmetry of inertia tensors, row- and column-major orders are equivalent. + + This quantity is parsed from the USD schema at the time of initialization. """ ## From de9e8ce0e53373e9b6b136a7ec6d42da6a51004a Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Tue, 9 Sep 2025 19:28:56 -0400 Subject: [PATCH 08/50] Adds new Collision Mesh Schema properties (#2249) # Description Adding new collision mesh property options allowing users to configure meshes to add different collision types ## Type of change - New feature (non-breaking change which adds functionality) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Harsh Patel Co-authored-by: James Tigue Co-authored-by: James Tigue <166445701+jtigue-bdai@users.noreply.github.com> Co-authored-by: Kelly Guo --- CONTRIBUTORS.md | 1 + .../isaaclab/sim/converters/mesh_converter.py | 9 +- .../sim/converters/mesh_converter_cfg.py | 19 +- .../isaaclab/isaaclab/sim/schemas/__init__.py | 65 ++++++ .../isaaclab/isaaclab/sim/schemas/schemas.py | 134 ++++++++++++ .../isaaclab/sim/schemas/schemas_cfg.py | 199 ++++++++++++++++++ source/isaaclab/isaaclab/utils/configclass.py | 8 + .../isaaclab/test/sim/test_mesh_converter.py | 27 ++- 8 files changed, 436 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 47335ecb0fb..ed704177acd 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -68,6 +68,7 @@ Guidelines for modifications: * Gary Lvov * Giulio Romualdi * Haoran Zhou +* Harsh Patel * HoJin Jeon * Hongwei Xiong * Hongyu Li diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index 45502e73351..c6c4683bbb8 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -122,14 +122,15 @@ def _convert_asset(self, cfg: MeshConverterCfg): if child_mesh_prim.GetTypeName() == "Mesh": # Apply collider properties to mesh if cfg.collision_props is not None: - # -- Collision approximation to mesh - # TODO: Move this to a new Schema: https://github.com/isaac-orbit/IsaacLab/issues/163 - mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(child_mesh_prim) - mesh_collision_api.GetApproximationAttr().Set(cfg.collision_approximation) # -- Collider properties such as offset, scale, etc. schemas.define_collision_properties( prim_path=child_mesh_prim.GetPath(), cfg=cfg.collision_props, stage=stage ) + # Add collision mesh + if cfg.mesh_collision_props is not None: + schemas.define_mesh_collision_properties( + prim_path=child_mesh_prim.GetPath(), cfg=cfg.mesh_collision_props, stage=stage + ) # Delete the old Xform and make the new Xform the default prim stage.SetDefaultPrim(xform_prim) # Apply default Xform rotation to mesh -> enable to set rotation and scale diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py index af639d941a1..97e66fd46e9 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter_cfg.py @@ -12,35 +12,30 @@ class MeshConverterCfg(AssetConverterBaseCfg): """The configuration class for MeshConverter.""" - mass_props: schemas_cfg.MassPropertiesCfg | None = None + mass_props: schemas_cfg.MassPropertiesCfg = None """Mass properties to apply to the USD. Defaults to None. Note: If None, then no mass properties will be added. """ - rigid_props: schemas_cfg.RigidBodyPropertiesCfg | None = None + rigid_props: schemas_cfg.RigidBodyPropertiesCfg = None """Rigid body properties to apply to the USD. Defaults to None. Note: If None, then no rigid body properties will be added. """ - collision_props: schemas_cfg.CollisionPropertiesCfg | None = None + collision_props: schemas_cfg.CollisionPropertiesCfg = None """Collision properties to apply to the USD. Defaults to None. Note: If None, then no collision properties will be added. """ - - collision_approximation: str = "convexDecomposition" - """Collision approximation method to use. Defaults to "convexDecomposition". - - Valid options are: - "convexDecomposition", "convexHull", "boundingCube", - "boundingSphere", "meshSimplification", or "none" - - "none" causes no collision mesh to be added. + mesh_collision_props: schemas_cfg.MeshCollisionPropertiesCfg = None + """Mesh approximation properties to apply to all collision meshes in the USD. + Note: + If None, then no mesh approximation properties will be added. """ translation: tuple[float, float, float] = (0.0, 0.0, 0.0) diff --git a/source/isaaclab/isaaclab/sim/schemas/__init__.py b/source/isaaclab/isaaclab/sim/schemas/__init__.py index bd78191ecf5..d8d04dfc478 100644 --- a/source/isaaclab/isaaclab/sim/schemas/__init__.py +++ b/source/isaaclab/isaaclab/sim/schemas/__init__.py @@ -33,11 +33,14 @@ """ from .schemas import ( + PHYSX_MESH_COLLISION_CFGS, + USD_MESH_COLLISION_CFGS, activate_contact_sensors, define_articulation_root_properties, define_collision_properties, define_deformable_body_properties, define_mass_properties, + define_mesh_collision_properties, define_rigid_body_properties, modify_articulation_root_properties, modify_collision_properties, @@ -45,16 +48,78 @@ modify_fixed_tendon_properties, modify_joint_drive_properties, modify_mass_properties, + modify_mesh_collision_properties, modify_rigid_body_properties, modify_spatial_tendon_properties, ) from .schemas_cfg import ( ArticulationRootPropertiesCfg, + BoundingCubePropertiesCfg, + BoundingSpherePropertiesCfg, CollisionPropertiesCfg, + ConvexDecompositionPropertiesCfg, + ConvexHullPropertiesCfg, DeformableBodyPropertiesCfg, FixedTendonPropertiesCfg, JointDrivePropertiesCfg, MassPropertiesCfg, + MeshCollisionPropertiesCfg, RigidBodyPropertiesCfg, + SDFMeshPropertiesCfg, SpatialTendonPropertiesCfg, + TriangleMeshPropertiesCfg, + TriangleMeshSimplificationPropertiesCfg, ) + +__all__ = [ + # articulation root + "ArticulationRootPropertiesCfg", + "define_articulation_root_properties", + "modify_articulation_root_properties", + # rigid bodies + "RigidBodyPropertiesCfg", + "define_rigid_body_properties", + "modify_rigid_body_properties", + "activate_contact_sensors", + # colliders + "CollisionPropertiesCfg", + "define_collision_properties", + "modify_collision_properties", + # deformables + "DeformableBodyPropertiesCfg", + "define_deformable_body_properties", + "modify_deformable_body_properties", + # joints + "JointDrivePropertiesCfg", + "modify_joint_drive_properties", + # mass + "MassPropertiesCfg", + "define_mass_properties", + "modify_mass_properties", + # mesh colliders + "MeshCollisionPropertiesCfg", + "define_mesh_collision_properties", + "modify_mesh_collision_properties", + # bounding cube + "BoundingCubePropertiesCfg", + # bounding sphere + "BoundingSpherePropertiesCfg", + # convex decomposition + "ConvexDecompositionPropertiesCfg", + # convex hull + "ConvexHullPropertiesCfg", + # sdf mesh + "SDFMeshPropertiesCfg", + # triangle mesh + "TriangleMeshPropertiesCfg", + # triangle mesh simplification + "TriangleMeshSimplificationPropertiesCfg", + # tendons + "FixedTendonPropertiesCfg", + "SpatialTendonPropertiesCfg", + "modify_fixed_tendon_properties", + "modify_spatial_tendon_properties", + # Constants for configs that use PhysX vs USD API + "PHYSX_MESH_COLLISION_CFGS", + "USD_MESH_COLLISION_CFGS", +] diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index a6003376122..482b6745842 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -26,6 +26,22 @@ Articulation root properties. """ +PHYSX_MESH_COLLISION_CFGS = [ + schemas_cfg.ConvexDecompositionPropertiesCfg, + schemas_cfg.ConvexHullPropertiesCfg, + schemas_cfg.TriangleMeshPropertiesCfg, + schemas_cfg.TriangleMeshSimplificationPropertiesCfg, + schemas_cfg.SDFMeshPropertiesCfg, +] + +USD_MESH_COLLISION_CFGS = [ + schemas_cfg.BoundingCubePropertiesCfg, + schemas_cfg.BoundingSpherePropertiesCfg, + schemas_cfg.ConvexDecompositionPropertiesCfg, + schemas_cfg.ConvexHullPropertiesCfg, + schemas_cfg.TriangleMeshSimplificationPropertiesCfg, +] + def define_articulation_root_properties( prim_path: str, cfg: schemas_cfg.ArticulationRootPropertiesCfg, stage: Usd.Stage | None = None @@ -934,3 +950,121 @@ def modify_deformable_body_properties( # success return True + + +""" +Collision mesh properties. +""" + + +def extract_mesh_collision_api_and_attrs(cfg): + # We use the number of user set attributes outside of the API function + # to determine which API to use in ambiguous cases, so collect them here + custom_attrs = { + key: value + for key, value in cfg.to_dict().items() + if value is not None and key not in ["usd_func", "physx_func"] + } + + use_usd_api = False + use_phsyx_api = False + + # We have some custom attributes and allow them + if len(custom_attrs) > 0 and type(cfg) in PHYSX_MESH_COLLISION_CFGS: + use_phsyx_api = True + # We have no custom attributes + elif len(custom_attrs) == 0: + if type(cfg) in USD_MESH_COLLISION_CFGS: + # Use the USD API + use_usd_api = True + else: + # Use the PhysX API + use_phsyx_api = True + + elif len(custom_attrs > 0) and type(cfg) in USD_MESH_COLLISION_CFGS: + raise ValueError("Args are specified but the USD Mesh API doesn't support them!") + + mesh_collision_appx_type = type(cfg).__name__.partition("PropertiesCfg")[0] + + if use_usd_api: + # Add approximation to the attributes as this is how USD collision mesh API is configured + api_func = cfg.usd_func + # Approximation needs to be formatted with camelCase + custom_attrs["Approximation"] = mesh_collision_appx_type[0].lower() + mesh_collision_appx_type[1:] + elif use_phsyx_api: + api_func = cfg.physx_func + else: + raise ValueError("Either USD or PhysX API should be used for mesh collision approximation!") + + return api_func, custom_attrs + + +def define_mesh_collision_properties( + prim_path: str, cfg: schemas_cfg.MeshCollisionPropertiesCfg, stage: Usd.Stage | None = None +): + """Apply the mesh collision schema on the input prim and set its properties. + See :func:`modify_collision_mesh_properties` for more details on how the properties are set. + Args: + prim_path : The prim path where to apply the mesh collision schema. + cfg : The configuration for the mesh collision properties. + stage : The stage where to find the prim. Defaults to None, in which case the + current stage is used. + Raises: + ValueError: When the prim path is not valid. + """ + # obtain stage + if stage is None: + stage = get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + + api_func, _ = extract_mesh_collision_api_and_attrs(cfg=cfg) + + # Only enable if not already enabled + if not api_func(prim): + api_func.Apply(prim) + + modify_mesh_collision_properties(prim_path=prim_path, cfg=cfg, stage=stage) + + +@apply_nested +def modify_mesh_collision_properties( + prim_path: str, cfg: schemas_cfg.MeshCollisionPropertiesCfg, stage: Usd.Stage | None = None +): + """Set properties for the mesh collision of a prim. + These properties are based on either the `Phsyx the `UsdPhysics.MeshCollisionAPI` schema. + .. note:: + This function is decorated with :func:`apply_nested` that sets the properties to all the prims + (that have the schema applied on them) under the input prim path. + .. UsdPhysics.MeshCollisionAPI: https://openusd.org/release/api/class_usd_physics_mesh_collision_a_p_i.html + Args: + prim_path : The prim path of the rigid body. This prim should be a Mesh prim. + cfg : The configuration for the mesh collision properties. + stage : The stage where to find the prim. Defaults to None, in which case the + current stage is used. + """ + # obtain stage + if stage is None: + stage = get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + + api_func, custom_attrs = extract_mesh_collision_api_and_attrs(cfg=cfg) + + # retrieve the mesh collision API + mesh_collision_api = api_func(prim) + + # set custom attributes into mesh collision API + for attr_name, value in custom_attrs.items(): + # Only "Attribute" attr should be in format "boundingSphere", so set camel_case to be False + if attr_name == "Attribute": + camel_case = False + else: + camel_case = True + safe_set_attribute_on_usd_schema(mesh_collision_api, attr_name, value, camel_case=camel_case) + + # success + return True diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py index 3fbd11cee22..a131f739e22 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas_cfg.py @@ -3,8 +3,11 @@ # # SPDX-License-Identifier: BSD-3-Clause +from dataclasses import MISSING from typing import Literal +from pxr import PhysxSchema, UsdPhysics + from isaaclab.utils import configclass @@ -426,3 +429,199 @@ class DeformableBodyPropertiesCfg: max_depenetration_velocity: float | None = None """Maximum depenetration velocity permitted to be introduced by the solver (in m/s).""" + + +@configclass +class MeshCollisionPropertiesCfg: + """Properties to apply to a mesh in regards to collision. + See :meth:`set_mesh_collision_properties` for more information. + + .. note:: + If the values are MISSING, they are not modified. This is useful when you want to set only a subset of + the properties and leave the rest as-is. + """ + + usd_func: callable = MISSING + + physx_func: callable = MISSING + + +@configclass +class BoundingCubePropertiesCfg(MeshCollisionPropertiesCfg): + usd_func: callable = UsdPhysics.MeshCollisionAPI + """Original USD Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html + """ + + +@configclass +class BoundingSpherePropertiesCfg(MeshCollisionPropertiesCfg): + usd_func: callable = UsdPhysics.MeshCollisionAPI + """Original USD Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html + """ + + +@configclass +class ConvexDecompositionPropertiesCfg(MeshCollisionPropertiesCfg): + usd_func: callable = UsdPhysics.MeshCollisionAPI + """Original USD Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html + """ + + physx_func: callable = PhysxSchema.PhysxConvexDecompositionCollisionAPI + """Original PhysX Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_convex_decomposition_collision_a_p_i.html + """ + + hull_vertex_limit: int | None = None + """Convex hull vertex limit used for convex hull cooking. + + Defaults to 64. + """ + max_convex_hulls: int | None = None + """Maximum of convex hulls created during convex decomposition. + Default value is 32. + """ + min_thickness: float | None = None + """Convex hull min thickness. + + Range: [0, inf). Units are distance. Default value is 0.001. + """ + voxel_resolution: int | None = None + """Voxel resolution used for convex decomposition. + + Defaults to 500,000 voxels. + """ + error_percentage: float | None = None + """Convex decomposition error percentage parameter. + + Defaults to 10 percent. Units are percent. + """ + shrink_wrap: bool | None = None + """Attempts to adjust the convex hull points so that they are projected onto the surface of the original graphics + mesh. + + Defaults to False. + """ + + +@configclass +class ConvexHullPropertiesCfg(MeshCollisionPropertiesCfg): + usd_func: callable = UsdPhysics.MeshCollisionAPI + """Original USD Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html + """ + + physx_func: callable = PhysxSchema.PhysxConvexHullCollisionAPI + """Original PhysX Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_convex_hull_collision_a_p_i.html + """ + + hull_vertex_limit: int | None = None + """Convex hull vertex limit used for convex hull cooking. + + Defaults to 64. + """ + min_thickness: float | None = None + """Convex hull min thickness. + + Range: [0, inf). Units are distance. Default value is 0.001. + """ + + +@configclass +class TriangleMeshPropertiesCfg(MeshCollisionPropertiesCfg): + physx_func: callable = PhysxSchema.PhysxTriangleMeshCollisionAPI + """Triangle mesh is only supported by PhysX API. + + Original PhysX Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_triangle_mesh_collision_a_p_i.html + """ + + weld_tolerance: float | None = None + """Mesh weld tolerance, controls the distance at which vertices are welded. + + Default -inf will autocompute the welding tolerance based on the mesh size. Zero value will disable welding. + Range: [0, inf) Units: distance + """ + + +@configclass +class TriangleMeshSimplificationPropertiesCfg(MeshCollisionPropertiesCfg): + usd_func: callable = UsdPhysics.MeshCollisionAPI + """Original USD Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_usd_physics_mesh_collision_a_p_i.html + """ + + physx_func: callable = PhysxSchema.PhysxTriangleMeshSimplificationCollisionAPI + """Original PhysX Documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_triangle_mesh_simplification_collision_a_p_i.html + """ + + simplification_metric: float | None = None + """Mesh simplification accuracy. + + Defaults to 0.55. + """ + weld_tolerance: float | None = None + """Mesh weld tolerance, controls the distance at which vertices are welded. + + Default -inf will autocompute the welding tolerance based on the mesh size. Zero value will disable welding. + Range: [0, inf) Units: distance + """ + + +@configclass +class SDFMeshPropertiesCfg(MeshCollisionPropertiesCfg): + physx_func: callable = PhysxSchema.PhysxSDFMeshCollisionAPI + """SDF mesh is only supported by PhysX API. + + Original PhysX documentation: + https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/latest/class_physx_schema_physx_s_d_f_mesh_collision_a_p_i.html + + More details and steps for optimizing SDF results can be found here: + https://nvidia-omniverse.github.io/PhysX/physx/5.2.1/docs/RigidBodyCollision.html#dynamic-triangle-meshes-with-sdfs + """ + sdf_margin: float | None = None + """Margin to increase the size of the SDF relative to the bounding box diagonal length of the mesh. + + + A sdf margin value of 0.01 means the sdf boundary will be enlarged in any direction by 1% of the mesh's bounding + box diagonal length. Representing the margin relative to the bounding box diagonal length ensures that it is scale + independent. Margins allow for precise distance queries in a region slightly outside of the mesh's bounding box. + + Default value is 0.01. + Range: [0, inf) Units: dimensionless + """ + sdf_narrow_band_thickness: float | None = None + """Size of the narrow band around the mesh surface where high resolution SDF samples are available. + + Outside of the narrow band, only low resolution samples are stored. Representing the narrow band thickness as a + fraction of the mesh's bounding box diagonal length ensures that it is scale independent. A value of 0.01 is + usually large enough. The smaller the narrow band thickness, the smaller the memory consumption of the sparse SDF. + + Default value is 0.01. + Range: [0, 1] Units: dimensionless + """ + sdf_resolution: int | None = None + """The spacing of the uniformly sampled SDF is equal to the largest AABB extent of the mesh, divided by the resolution. + + Choose the lowest possible resolution that provides acceptable performance; very high resolution results in large + memory consumption, and slower cooking and simulation performance. + + Default value is 256. + Range: (1, inf) + """ + sdf_subgrid_resolution: int | None = None + """A positive subgrid resolution enables sparsity on signed-distance-fields (SDF) while a value of 0 leads to the + usage of a dense SDF. + + A value in the range of 4 to 8 is a reasonable compromise between block size and the overhead introduced by block + addressing. The smaller a block, the more memory is spent on the address table. The bigger a block, the less + precisely the sparse SDF can adapt to the mesh's surface. In most cases sparsity reduces the memory consumption of + a SDF significantly. + + Default value is 6. + Range: [0, inf) + """ diff --git a/source/isaaclab/isaaclab/utils/configclass.py b/source/isaaclab/isaaclab/utils/configclass.py index 091b9862474..bce95d961c7 100644 --- a/source/isaaclab/isaaclab/utils/configclass.py +++ b/source/isaaclab/isaaclab/utils/configclass.py @@ -259,6 +259,9 @@ def _validate(obj: object, prefix: str = "") -> list[str]: """ missing_fields = [] + if type(obj).__name__ == "MeshConverterCfg": + return missing_fields + if type(obj) is type(MISSING): missing_fields.append(prefix) return missing_fields @@ -455,10 +458,15 @@ def _skippable_class_member(key: str, value: Any, hints: dict | None = None) -> # check for class methods if isinstance(value, types.MethodType): return True + + if "CollisionAPI" in value.__name__: + return False + # check for instance methods signature = inspect.signature(value) if "self" in signature.parameters or "cls" in signature.parameters: return True + # skip property methods if isinstance(value, property): return True diff --git a/source/isaaclab/test/sim/test_mesh_converter.py b/source/isaaclab/test/sim/test_mesh_converter.py index 9e0085a065d..90bfc557c78 100644 --- a/source/isaaclab/test/sim/test_mesh_converter.py +++ b/source/isaaclab/test/sim/test_mesh_converter.py @@ -132,10 +132,13 @@ def check_mesh_collider_settings(mesh_converter: MeshConverter): assert collision_enabled == exp_collision_enabled, "Collision enabled is not the same!" # -- if collision is enabled, check that collision approximation is correct if exp_collision_enabled: - exp_collision_approximation = mesh_converter.cfg.collision_approximation - mesh_collision_api = UsdPhysics.MeshCollisionAPI(mesh_prim) - collision_approximation = mesh_collision_api.GetApproximationAttr().Get() - assert collision_approximation == exp_collision_approximation, "Collision approximation is not the same!" + if mesh_converter.cfg.mesh_collision_props is not None: + exp_collision_approximation = ( + mesh_converter.cfg.mesh_collision_props.usd_func(mesh_prim).GetApproximationAttr().Get() + ) + mesh_collision_api = UsdPhysics.MeshCollisionAPI(mesh_prim) + collision_approximation = mesh_collision_api.GetApproximationAttr().Get() + assert collision_approximation == exp_collision_approximation, "Collision approximation is not the same!" def test_no_change(assets): @@ -229,7 +232,6 @@ def test_collider_no_approximation(assets): collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) mesh_config = MeshConverterCfg( asset_path=assets["obj"], - collision_approximation="none", collision_props=collision_props, ) mesh_converter = MeshConverter(mesh_config) @@ -241,9 +243,10 @@ def test_collider_no_approximation(assets): def test_collider_convex_hull(assets): """Convert an OBJ file using convex hull approximation""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.ConvexHullPropertiesCfg() mesh_config = MeshConverterCfg( asset_path=assets["obj"], - collision_approximation="convexHull", + mesh_collision_props=mesh_collision_prop, collision_props=collision_props, ) mesh_converter = MeshConverter(mesh_config) @@ -255,9 +258,10 @@ def test_collider_convex_hull(assets): def test_collider_mesh_simplification(assets): """Convert an OBJ file using mesh simplification approximation""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.TriangleMeshSimplificationPropertiesCfg() mesh_config = MeshConverterCfg( asset_path=assets["obj"], - collision_approximation="meshSimplification", + mesh_collision_props=mesh_collision_prop, collision_props=collision_props, ) mesh_converter = MeshConverter(mesh_config) @@ -269,9 +273,10 @@ def test_collider_mesh_simplification(assets): def test_collider_mesh_bounding_cube(assets): """Convert an OBJ file using bounding cube approximation""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.BoundingCubePropertiesCfg() mesh_config = MeshConverterCfg( asset_path=assets["obj"], - collision_approximation="boundingCube", + mesh_collision_props=mesh_collision_prop, collision_props=collision_props, ) mesh_converter = MeshConverter(mesh_config) @@ -283,9 +288,10 @@ def test_collider_mesh_bounding_cube(assets): def test_collider_mesh_bounding_sphere(assets): """Convert an OBJ file using bounding sphere""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=True) + mesh_collision_prop = schemas_cfg.BoundingSpherePropertiesCfg() mesh_config = MeshConverterCfg( asset_path=assets["obj"], - collision_approximation="boundingSphere", + mesh_collision_props=mesh_collision_prop, collision_props=collision_props, ) mesh_converter = MeshConverter(mesh_config) @@ -297,9 +303,10 @@ def test_collider_mesh_bounding_sphere(assets): def test_collider_mesh_no_collision(assets): """Convert an OBJ file using bounding sphere with collision disabled""" collision_props = schemas_cfg.CollisionPropertiesCfg(collision_enabled=False) + mesh_collision_prop = schemas_cfg.BoundingSpherePropertiesCfg() mesh_config = MeshConverterCfg( asset_path=assets["obj"], - collision_approximation="boundingSphere", + mesh_collision_props=mesh_collision_prop, collision_props=collision_props, ) mesh_converter = MeshConverter(mesh_config) From c7dde1b7972639cfbb29e438d24fc191cf5dbc94 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Tue, 9 Sep 2025 16:29:09 -0700 Subject: [PATCH 09/50] Adds dexterous lift and reorientation manipulation environments (#3378) # Description This PR provides remake and extension to orginal environment kuka-allegro-reorientation implemented in paper: DexPBT: Scaling up Dexterous Manipulation for Hand-Arm Systems with Population Based Training (https://arxiv.org/abs/2305.12127) [Aleksei Petrenko](https://arxiv.org/search/cs?searchtype=author&query=Petrenko,+A), [Arthur Allshire](https://arxiv.org/search/cs?searchtype=author&query=Allshire,+A), [Gavriel State](https://arxiv.org/search/cs?searchtype=author&query=State,+G), [Ankur Handa](https://arxiv.org/search/cs?searchtype=author&query=Handa,+A), [Viktor Makoviychuk](https://arxiv.org/search/cs?searchtype=author&query=Makoviychuk,+V) and another environment kuka-allegro-lift implemented in paper: Visuomotor Policies to Grasp Anything with Dexterous Hands (https://dextrah-rgb.github.io/) [Ritvik Singh](https://www.ritvik-singh.com/), [Arthur Allshire](https://allshire.org/), [Ankur Handa](https://ankurhanda.github.io/), [Nathan Ratliff](https://www.nathanratliff.com/), [Karl Van Wyk](https://scholar.google.com/citations?user=TCYAoF8AAAAJ&hl=en) Though this is a remake, this remake ends up differs quite a lot in environment details for reasons like: 1. Simplify reward structure, 2. Unify environment implemtation, 3. Standarize mdp, 4. Utilizes manager-based API That in my opinion, makes environment study and extension more accessible, and analyzable. For example you can train lift policy first then continuing the checkpoint in reorientation environment, since they share the observation space. : )) It is a best to consider this a very careful re-interpretation rather than exact execution to migrate them to IsaacLab Here is the training curve if you just train with `./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Dexsuite-Kuka-Allegro-Lift-v0 --num_envs 8192 --headless` `./isaaclab.sh -p -m torch.distributed.run --nnodes=1 --nproc_per_node=4 scripts/reinforcement_learning/rsl_rl/train.py --task Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 --num_envs 40960 --headless --distributed` lift training ~ 4 hours reorientation training ~ 2 days Note that it requires a order of magnitude more data and time for reorientation to converge compare to lift under almost identical setup training curve(screen captured from Wandb) - reward, Cyan: reorient, Purple: Lift Screenshot from 2025-09-07 22-58-13 video results lift ![cone_lift](https://github.com/user-attachments/assets/e626eadb-b281-4ec9-af16-57f626fcc6aa) ![fat_capsule_lift](https://github.com/user-attachments/assets/cde57d4c-ceb2-40ab-88dd-44320da689c5) reorient ![cube_reorient](https://github.com/user-attachments/assets/752809cb-ea19-4701-b124-20c1909e4566) ![rod_reorient](https://github.com/user-attachments/assets/f009605a-d93c-491c-b124-ff08606c63ec) Memo: I really enjoy working on this remake, and hopefully for whoever plan to play and extend on this remake find it helpful and similarily joyful as I did. I will be very excited to see what you got : )) Octi CAUTION: Do Not Merge until the asset is uploaded to S3 bucket! Fixes # (issue) - New feature (non-breaking change which adds functionality) ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../tasks/manipulation/kuka_allegro_lift.jpg | Bin 0 -> 47720 bytes .../manipulation/kuka_allegro_reorient.jpg | Bin 0 -> 52080 bytes docs/source/overview/environments.rst | 109 ++-- .../isaaclab_assets/robots/__init__.py | 1 + .../isaaclab_assets/robots/kuka_allegro.py | 114 +++++ source/isaaclab_tasks/config/extension.toml | 2 +- source/isaaclab_tasks/docs/CHANGELOG.rst | 9 + .../manipulation/dexsuite/__init__.py | 26 + .../manipulation/dexsuite/adr_curriculum.py | 122 +++++ .../manipulation/dexsuite/config/__init__.py | 9 + .../dexsuite/config/kuka_allegro/__init__.py | 63 +++ .../config/kuka_allegro/agents/__init__.py | 4 + .../kuka_allegro/agents/rl_games_ppo_cfg.yaml | 86 ++++ .../kuka_allegro/agents/rsl_rl_ppo_cfg.py | 39 ++ .../dexsuite_kuka_allegro_env_cfg.py | 79 +++ .../manipulation/dexsuite/dexsuite_env_cfg.py | 466 ++++++++++++++++++ .../manipulation/dexsuite/mdp/__init__.py | 12 + .../dexsuite/mdp/commands/__init__.py | 6 + .../dexsuite/mdp/commands/pose_commands.py | 179 +++++++ .../mdp/commands/pose_commands_cfg.py | 92 ++++ .../manipulation/dexsuite/mdp/curriculums.py | 113 +++++ .../manipulation/dexsuite/mdp/observations.py | 197 ++++++++ .../manipulation/dexsuite/mdp/rewards.py | 126 +++++ .../manipulation/dexsuite/mdp/terminations.py | 49 ++ .../manipulation/dexsuite/mdp/utils.py | 247 ++++++++++ 25 files changed, 2101 insertions(+), 49 deletions(-) create mode 100644 docs/source/_static/tasks/manipulation/kuka_allegro_lift.jpg create mode 100644 docs/source/_static/tasks/manipulation/kuka_allegro_reorient.jpg create mode 100644 source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py diff --git a/docs/source/_static/tasks/manipulation/kuka_allegro_lift.jpg b/docs/source/_static/tasks/manipulation/kuka_allegro_lift.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9d19b0e423667ae1a3760414fa38a56636d5209d GIT binary patch literal 47720 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

ph!=-DNtL1Tk;SsGMM0Rc~%xo%Q0kJTh(=@Oj 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 JG_PN?{|^hzP|*MY literal 0 HcmV?d00001 diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index c4925adfb94..afc1fdc0e99 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -100,51 +100,55 @@ for the lift-cube environment: .. table:: :widths: 33 37 30 - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | World | Environment ID | Description | - +======================+===========================+=============================================================================+ - | |reach-franka| | |reach-franka-link| | Move the end-effector to a sampled target pose with the Franka robot | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |reach-ur10| | |reach-ur10-link| | Move the end-effector to a sampled target pose with the UR10 robot | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |deploy-reach-ur10e| | |deploy-reach-ur10e-link| | Move the end-effector to a sampled target pose with the UR10e robot | - | | | This policy has been deployed to a real robot | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |lift-cube| | |lift-cube-link| | Pick a cube and bring it to a sampled target position with the Franka robot | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |stack-cube| | |stack-cube-link| | Stack three cubes (bottom to top: blue, red, green) with the Franka robot. | - | | | Blueprint env used for the NVIDIA Isaac GR00T blueprint for synthetic | - | | |stack-cube-bp-link| | manipulation motion generation | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |surface-gripper| | |long-suction-link| | Stack three cubes (bottom to top: blue, red, green) | - | | | with the UR10 arm and long surface gripper | - | | |short-suction-link| | or short surface gripper. | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | - | | | | - | | |franka-direct-link| | | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |cube-allegro| | |cube-allegro-link| | In-hand reorientation of a cube using Allegro hand | - | | | | - | | |allegro-direct-link| | | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |cube-shadow| | |cube-shadow-link| | In-hand reorientation of a cube using Shadow hand | - | | | | - | | |cube-shadow-ff-link| | | - | | | | - | | |cube-shadow-lstm-link| | | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |cube-shadow| | |cube-shadow-vis-link| | In-hand reorientation of a cube using Shadow hand using perceptive inputs. | - | | | Requires running with ``--enable_cameras``. | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |gr1_pick_place| | |gr1_pick_place-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |gr1_pp_waist| | |gr1_pp_waist-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | - | | | with waist degrees-of-freedom enables that provides a wider reach space. | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ - | |galbot_stack| | |galbot_stack-link| | Stack three cubes (bottom to top: blue, red, green) with the left arm of | - | | | a Galbot humanoid robot | - +----------------------+---------------------------+-----------------------------------------------------------------------------+ + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +=========================+==============================+=============================================================================+ + | |reach-franka| | |reach-franka-link| | Move the end-effector to a sampled target pose with the Franka robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |reach-ur10| | |reach-ur10-link| | Move the end-effector to a sampled target pose with the UR10 robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |deploy-reach-ur10e| | |deploy-reach-ur10e-link| | Move the end-effector to a sampled target pose with the UR10e robot | + | | | This policy has been deployed to a real robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |lift-cube| | |lift-cube-link| | Pick a cube and bring it to a sampled target position with the Franka robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |stack-cube| | |stack-cube-link| | Stack three cubes (bottom to top: blue, red, green) with the Franka robot. | + | | | Blueprint env used for the NVIDIA Isaac GR00T blueprint for synthetic | + | | |stack-cube-bp-link| | manipulation motion generation | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |surface-gripper| | |long-suction-link| | Stack three cubes (bottom to top: blue, red, green) | + | | | with the UR10 arm and long surface gripper | + | | |short-suction-link| | or short surface gripper. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | + | | | | + | | |franka-direct-link| | | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cube-allegro| | |cube-allegro-link| | In-hand reorientation of a cube using Allegro hand | + | | | | + | | |allegro-direct-link| | | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cube-shadow| | |cube-shadow-link| | In-hand reorientation of a cube using Shadow hand | + | | | | + | | |cube-shadow-ff-link| | | + | | | | + | | |cube-shadow-lstm-link| | | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |cube-shadow| | |cube-shadow-vis-link| | In-hand reorientation of a cube using Shadow hand using perceptive inputs. | + | | | Requires running with ``--enable_cameras``. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |gr1_pick_place| | |gr1_pick_place-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |gr1_pp_waist| | |gr1_pp_waist-link| | Pick up and place an object in a basket with a GR-1 humanoid robot | + | | | with waist degrees-of-freedom enables that provides a wider reach space. | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |kuka-allegro-lift| | |kuka-allegro-lift-link| | Pick up a primitive shape on the table and lift it to target position | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |kuka-allegro-reorient| | |kuka-allegro-reorient-link| | Pick up a primitive shape on the table and orient it to target pose | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ + | |galbot_stack| | |galbot_stack-link| | Stack three cubes (bottom to top: blue, red, green) with the left arm of | + | | | a Galbot humanoid robot | + +-------------------------+------------------------------+-----------------------------------------------------------------------------+ .. |reach-franka| image:: ../_static/tasks/manipulation/franka_reach.jpg .. |reach-ur10| image:: ../_static/tasks/manipulation/ur10_reach.jpg @@ -157,8 +161,9 @@ for the lift-cube environment: .. |gr1_pick_place| image:: ../_static/tasks/manipulation/gr-1_pick_place.jpg .. |surface-gripper| image:: ../_static/tasks/manipulation/ur10_stack_surface_gripper.jpg .. |gr1_pp_waist| image:: ../_static/tasks/manipulation/gr-1_pick_place_waist.jpg -.. |surface-gripper| image:: ../_static/tasks/manipulation/ur10_stack_surface_gripper.jpg .. |galbot_stack| image:: ../_static/tasks/manipulation/galbot_stack_cube.jpg +.. |kuka-allegro-lift| image:: ../_static/tasks/manipulation/kuka_allegro_lift.jpg +.. |kuka-allegro-reorient| image:: ../_static/tasks/manipulation/kuka_allegro_reorient.jpg .. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ .. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ @@ -177,8 +182,8 @@ for the lift-cube environment: .. |short-suction-link| replace:: `Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 `__ .. |gr1_pp_waist-link| replace:: `Isaac-PickPlace-GR1T2-WaistEnabled-Abs-v0 `__ .. |galbot_stack-link| replace:: `Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 `__ -.. |long-suction-link| replace:: `Isaac-Stack-Cube-UR10-Long-Suction-IK-Rel-v0 `__ -.. |short-suction-link| replace:: `Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 `__ +.. |kuka-allegro-lift-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Lift-v0 `__ +.. |kuka-allegro-reorient-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 `__ .. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ .. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ @@ -942,6 +947,14 @@ inferencing, including reading from an already trained checkpoint and disabling - - Manager Based - + * - Isaac-Dexsuite-Kuka-Allegro-Lift-v0 + - Isaac-Dexsuite-Kuka-Allegro-Lift-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO) + * - Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 + - Isaac-Dexsuite-Kuka-Allegro-Reorient-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO) * - Isaac-Stack-Cube-Franka-v0 - - Manager Based diff --git a/source/isaaclab_assets/isaaclab_assets/robots/__init__.py b/source/isaaclab_assets/isaaclab_assets/robots/__init__.py index a5996104680..82a13a05e49 100644 --- a/source/isaaclab_assets/isaaclab_assets/robots/__init__.py +++ b/source/isaaclab_assets/isaaclab_assets/robots/__init__.py @@ -18,6 +18,7 @@ from .humanoid import * from .humanoid_28 import * from .kinova import * +from .kuka_allegro import * from .pick_and_place import * from .quadcopter import * from .ridgeback_franka import * diff --git a/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py b/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py new file mode 100644 index 00000000000..d6c86bb3f15 --- /dev/null +++ b/source/isaaclab_assets/isaaclab_assets/robots/kuka_allegro.py @@ -0,0 +1,114 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configuration for the Kuka-lbr-iiwa arm robots and Allegro Hand. + +The following configurations are available: + +* :obj:`KUKA_ALLEGRO_CFG`: Kuka Allegro with implicit actuator model. + +Reference: + +* https://www.kuka.com/en-us/products/robotics-systems/industrial-robots/lbr-iiwa +* https://www.wonikrobotics.com/robot-hand + +""" + +import isaaclab.sim as sim_utils +from isaaclab.actuators.actuator_cfg import ImplicitActuatorCfg +from isaaclab.assets.articulation import ArticulationCfg +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR + +## +# Configuration +## + +KUKA_ALLEGRO_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/KukaAllegro/kuka.usd", + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + retain_accelerations=True, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1000.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, + solver_position_iteration_count=32, + solver_velocity_iteration_count=1, + sleep_threshold=0.005, + stabilization_threshold=0.0005, + ), + joint_drive_props=sim_utils.JointDrivePropertiesCfg(drive_type="force"), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.0), + rot=(1.0, 0.0, 0.0, 0.0), + joint_pos={ + "iiwa7_joint_(1|2|7)": 0.0, + "iiwa7_joint_3": 0.7854, + "iiwa7_joint_4": 1.5708, + "iiwa7_joint_(5|6)": -1.5708, + "(index|middle|ring)_joint_0": 0.0, + "(index|middle|ring)_joint_1": 0.3, + "(index|middle|ring)_joint_2": 0.3, + "(index|middle|ring)_joint_3": 0.3, + "thumb_joint_0": 1.5, + "thumb_joint_1": 0.60147215, + "thumb_joint_2": 0.33795027, + "thumb_joint_3": 0.60845138, + }, + ), + actuators={ + "kuka_allegro_actuators": ImplicitActuatorCfg( + joint_names_expr=[ + "iiwa7_joint_(1|2|3|4|5|6|7)", + "index_joint_(0|1|2|3)", + "middle_joint_(0|1|2|3)", + "ring_joint_(0|1|2|3)", + "thumb_joint_(0|1|2|3)", + ], + effort_limit_sim={ + "iiwa7_joint_(1|2|3|4|5|6|7)": 300.0, + "index_joint_(0|1|2|3)": 0.5, + "middle_joint_(0|1|2|3)": 0.5, + "ring_joint_(0|1|2|3)": 0.5, + "thumb_joint_(0|1|2|3)": 0.5, + }, + stiffness={ + "iiwa7_joint_(1|2|3|4)": 300.0, + "iiwa7_joint_5": 100.0, + "iiwa7_joint_6": 50.0, + "iiwa7_joint_7": 25.0, + "index_joint_(0|1|2|3)": 3.0, + "middle_joint_(0|1|2|3)": 3.0, + "ring_joint_(0|1|2|3)": 3.0, + "thumb_joint_(0|1|2|3)": 3.0, + }, + damping={ + "iiwa7_joint_(1|2|3|4)": 45.0, + "iiwa7_joint_5": 20.0, + "iiwa7_joint_6": 15.0, + "iiwa7_joint_7": 15.0, + "index_joint_(0|1|2|3)": 0.1, + "middle_joint_(0|1|2|3)": 0.1, + "ring_joint_(0|1|2|3)": 0.1, + "thumb_joint_(0|1|2|3)": 0.1, + }, + friction={ + "iiwa7_joint_(1|2|3|4|5|6|7)": 1.0, + "index_joint_(0|1|2|3)": 0.01, + "middle_joint_(0|1|2|3)": 0.01, + "ring_joint_(0|1|2|3)": 0.01, + "thumb_joint_(0|1|2|3)": 0.01, + }, + ), + }, + soft_joint_pos_limit_factor=1.0, +) diff --git a/source/isaaclab_tasks/config/extension.toml b/source/isaaclab_tasks/config/extension.toml index f317365d688..1a6d1b88d07 100644 --- a/source/isaaclab_tasks/config/extension.toml +++ b/source/isaaclab_tasks/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.10.51" +version = "0.11.0" # Description title = "Isaac Lab Environments" diff --git a/source/isaaclab_tasks/docs/CHANGELOG.rst b/source/isaaclab_tasks/docs/CHANGELOG.rst index ee84acbafd5..cda01d77b03 100644 --- a/source/isaaclab_tasks/docs/CHANGELOG.rst +++ b/source/isaaclab_tasks/docs/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog --------- +0.11.0 (2025-09-07) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added dextrous lifting and dextrous reorientation manipulation rl environments. + + 0.10.51 (2025-09-08) ~~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py new file mode 100644 index 00000000000..26075d4da25 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Dexsuite environments. + +Implementation Reference: + +Reorient: +@article{petrenko2023dexpbt, + title={Dexpbt: Scaling up dexterous manipulation for hand-arm systems with population based training}, + author={Petrenko, Aleksei and Allshire, Arthur and State, Gavriel and Handa, Ankur and Makoviychuk, Viktor}, + journal={arXiv preprint arXiv:2305.12127}, + year={2023} +} + +Lift: +@article{singh2024dextrah, + title={Dextrah-rgb: Visuomotor policies to grasp anything with dexterous hands}, + author={Singh, Ritvik and Allshire, Arthur and Handa, Ankur and Ratliff, Nathan and Van Wyk, Karl}, + journal={arXiv preprint arXiv:2412.01791}, + year={2024} +} + +""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py new file mode 100644 index 00000000000..52fef8b494a --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/adr_curriculum.py @@ -0,0 +1,122 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.managers import CurriculumTermCfg as CurrTerm +from isaaclab.utils import configclass + +from . import mdp + + +@configclass +class CurriculumCfg: + """Curriculum terms for the MDP.""" + + # adr stands for automatic/adaptive domain randomization + adr = CurrTerm( + func=mdp.DifficultyScheduler, params={"init_difficulty": 0, "min_difficulty": 0, "max_difficulty": 10} + ) + + joint_pos_unoise_min_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.proprio.joint_pos.noise.n_min", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": -0.1, "difficulty_term_str": "adr"}, + }, + ) + + joint_pos_unoise_max_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.proprio.joint_pos.noise.n_max", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": 0.1, "difficulty_term_str": "adr"}, + }, + ) + + joint_vel_unoise_min_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.proprio.joint_vel.noise.n_min", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": -0.2, "difficulty_term_str": "adr"}, + }, + ) + + joint_vel_unoise_max_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.proprio.joint_vel.noise.n_max", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": 0.2, "difficulty_term_str": "adr"}, + }, + ) + + hand_tips_pos_unoise_min_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.proprio.hand_tips_state_b.noise.n_min", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": -0.01, "difficulty_term_str": "adr"}, + }, + ) + + hand_tips_pos_unoise_max_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.proprio.hand_tips_state_b.noise.n_max", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": 0.01, "difficulty_term_str": "adr"}, + }, + ) + + object_quat_unoise_min_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.policy.object_quat_b.noise.n_min", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": -0.03, "difficulty_term_str": "adr"}, + }, + ) + + object_quat_unoise_max_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.policy.object_quat_b.noise.n_max", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": 0.03, "difficulty_term_str": "adr"}, + }, + ) + + object_obs_unoise_min_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.perception.object_point_cloud.noise.n_min", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": -0.01, "difficulty_term_str": "adr"}, + }, + ) + + object_obs_unoise_max_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "observations.perception.object_point_cloud.noise.n_max", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": {"initial_value": 0.0, "final_value": -0.01, "difficulty_term_str": "adr"}, + }, + ) + + gravity_adr = CurrTerm( + func=mdp.modify_term_cfg, + params={ + "address": "events.variable_gravity.params.gravity_distribution_params", + "modify_fn": mdp.initial_final_interpolate_fn, + "modify_params": { + "initial_value": ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), + "final_value": ((0.0, 0.0, -9.81), (0.0, 0.0, -9.81)), + "difficulty_term_str": "adr", + }, + }, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py new file mode 100644 index 00000000000..4240e604428 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for the dexsuite environments.""" + +# We leave this file empty since we don't want to expose any configs in this package directly. +# We still need this file to import the "config" module in the parent package. diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py new file mode 100644 index 00000000000..159ab6727fb --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/__init__.py @@ -0,0 +1,63 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +""" +Dextra Kuka Allegro environments. +""" + +import gymnasium as gym + +from . import agents + +## +# Register Gym environments. +## + +# State Observation +gym.register( + id="Isaac-Dexsuite-Kuka-Allegro-Reorient-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.dexsuite_kuka_allegro_env_cfg:DexsuiteKukaAllegroReorientEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DexsuiteKukaAllegroPPORunnerCfg", + }, +) + +gym.register( + id="Isaac-Dexsuite-Kuka-Allegro-Reorient-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.dexsuite_kuka_allegro_env_cfg:DexsuiteKukaAllegroReorientEnvCfg_PLAY", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DexsuiteKukaAllegroPPORunnerCfg", + }, +) + +# Dexsuite Lift Environments +gym.register( + id="Isaac-Dexsuite-Kuka-Allegro-Lift-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.dexsuite_kuka_allegro_env_cfg:DexsuiteKukaAllegroLiftEnvCfg", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DexsuiteKukaAllegroPPORunnerCfg", + }, +) + + +gym.register( + id="Isaac-Dexsuite-Kuka-Allegro-Lift-Play-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": f"{__name__}.dexsuite_kuka_allegro_env_cfg:DexsuiteKukaAllegroLiftEnvCfg_PLAY", + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:DexsuiteKukaAllegroPPORunnerCfg", + }, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py new file mode 100644 index 00000000000..2e924fbf1b1 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 00000000000..3d23b745cc5 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,86 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +params: + seed: 42 + + # environment wrapper clipping + env: + clip_observations: 100.0 + clip_actions: 100.0 + obs_groups: + obs: ["policy", "proprio", "perception"] + states: ["policy", "proprio", "perception"] + concate_obs_groups: True + + algo: + name: a2c_continuous + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: True + space: + continuous: + mu_activation: None + sigma_activation: None + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: True + mlp: + units: [512, 256, 128] + activation: elu + d2rl: False + initializer: + name: default + regularizer: + name: None + + load_checkpoint: False # flag which sets whether to load the checkpoint + load_path: '' # path to the checkpoint to load + + config: + name: reorient + env_name: rlgpu + device: 'cuda:0' + device_name: 'cuda:0' + multi_gpu: False + ppo: True + mixed_precision: False + normalize_input: True + normalize_value: True + value_bootstrap: False + num_actors: -1 + reward_shaper: + scale_value: 0.01 + normalize_advantage: True + gamma: 0.99 + tau: 0.95 + learning_rate: 1e-3 + lr_schedule: adaptive + schedule_type: legacy + kl_threshold: 0.01 + score_to_win: 100000000 + max_epochs: 750000 + save_best_after: 100 + save_frequency: 50 + print_stats: True + grad_norm: 1.0 + entropy_coef: 0.001 + truncate_grads: True + e_clip: 0.2 + horizon_length: 36 + minibatch_size: 36864 + mini_epochs: 5 + critic_coef: 4 + clip_value: True + clip_actions: False + seq_len: 4 + bounds_loss_coef: 0.0001 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py new file mode 100644 index 00000000000..f7965575737 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_ppo_cfg.py @@ -0,0 +1,39 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + +from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +@configclass +class DexsuiteKukaAllegroPPORunnerCfg(RslRlOnPolicyRunnerCfg): + num_steps_per_env = 32 + obs_groups = {"policy": ["policy", "proprio", "perception"], "critic": ["policy", "proprio", "perception"]} + max_iterations = 15000 + save_interval = 250 + experiment_name = "dexsuite_kuka_allegro" + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_obs_normalization=True, + critic_obs_normalization=True, + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], + activation="elu", + ) + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.005, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py new file mode 100644 index 00000000000..6c41414f30b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py @@ -0,0 +1,79 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab_assets.robots import KUKA_ALLEGRO_CFG + +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import ContactSensorCfg +from isaaclab.utils import configclass + +from ... import dexsuite_env_cfg as dexsuite +from ... import mdp + + +@configclass +class KukaAllegroRelJointPosActionCfg: + action = mdp.RelativeJointPositionActionCfg(asset_name="robot", joint_names=[".*"], scale=0.1) + + +@configclass +class KukaAllegroReorientRewardCfg(dexsuite.RewardsCfg): + + # bool awarding term if 2 finger tips are in contact with object, one of the contacting fingers has to be thumb. + good_finger_contact = RewTerm( + func=mdp.contacts, + weight=0.5, + params={"threshold": 1.0}, + ) + + +@configclass +class KukaAllegroMixinCfg: + rewards: KukaAllegroReorientRewardCfg = KukaAllegroReorientRewardCfg() + actions: KukaAllegroRelJointPosActionCfg = KukaAllegroRelJointPosActionCfg() + + def __post_init__(self: dexsuite.DexsuiteReorientEnvCfg): + super().__post_init__() + self.commands.object_pose.body_name = "palm_link" + self.scene.robot = KUKA_ALLEGRO_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + finger_tip_body_list = ["index_link_3", "middle_link_3", "ring_link_3", "thumb_link_3"] + for link_name in finger_tip_body_list: + setattr( + self.scene, + f"{link_name}_object_s", + ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/ee_link/" + link_name, + filter_prim_paths_expr=["{ENV_REGEX_NS}/Object"], + ), + ) + self.observations.proprio.contact = ObsTerm( + func=mdp.fingers_contact_force_b, + params={"contact_sensor_names": [f"{link}_object_s" for link in finger_tip_body_list]}, + clip=(-20.0, 20.0), # contact force in finger tips is under 20N normally + ) + self.observations.proprio.hand_tips_state_b.params["body_asset_cfg"].body_names = ["palm_link", ".*_tip"] + self.rewards.fingers_to_object.params["asset_cfg"] = SceneEntityCfg("robot", body_names=["palm_link", ".*_tip"]) + + +@configclass +class DexsuiteKukaAllegroReorientEnvCfg(KukaAllegroMixinCfg, dexsuite.DexsuiteReorientEnvCfg): + pass + + +@configclass +class DexsuiteKukaAllegroReorientEnvCfg_PLAY(KukaAllegroMixinCfg, dexsuite.DexsuiteReorientEnvCfg_PLAY): + pass + + +@configclass +class DexsuiteKukaAllegroLiftEnvCfg(KukaAllegroMixinCfg, dexsuite.DexsuiteLiftEnvCfg): + pass + + +@configclass +class DexsuiteKukaAllegroLiftEnvCfg_PLAY(KukaAllegroMixinCfg, dexsuite.DexsuiteLiftEnvCfg_PLAY): + pass diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py new file mode 100644 index 00000000000..75e40c5c74b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py @@ -0,0 +1,466 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg +from isaaclab.envs import ManagerBasedEnvCfg, ViewerCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sim import CapsuleCfg, ConeCfg, CuboidCfg, RigidBodyMaterialCfg, SphereCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +from . import mdp +from .adr_curriculum import CurriculumCfg + + +@configclass +class SceneCfg(InteractiveSceneCfg): + """Dexsuite Scene for multi-objects Lifting""" + + # robot + robot: ArticulationCfg = MISSING + + # object + object: RigidObjectCfg = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Object", + spawn=sim_utils.MultiAssetSpawnerCfg( + assets_cfg=[ + CuboidCfg(size=(0.05, 0.1, 0.1), physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CuboidCfg(size=(0.05, 0.05, 0.1), physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CuboidCfg(size=(0.025, 0.1, 0.1), physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CuboidCfg(size=(0.025, 0.05, 0.1), physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CuboidCfg(size=(0.025, 0.025, 0.1), physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CuboidCfg(size=(0.01, 0.1, 0.1), physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + SphereCfg(radius=0.05, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + SphereCfg(radius=0.025, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CapsuleCfg(radius=0.04, height=0.025, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CapsuleCfg(radius=0.04, height=0.01, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CapsuleCfg(radius=0.04, height=0.1, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CapsuleCfg(radius=0.025, height=0.1, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CapsuleCfg(radius=0.025, height=0.2, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + CapsuleCfg(radius=0.01, height=0.2, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + ConeCfg(radius=0.05, height=0.1, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + ConeCfg(radius=0.025, height=0.1, physics_material=RigidBodyMaterialCfg(static_friction=0.5)), + ], + rigid_props=sim_utils.RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=0, + disable_gravity=False, + ), + collision_props=sim_utils.CollisionPropertiesCfg(), + mass_props=sim_utils.MassPropertiesCfg(mass=0.2), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.55, 0.1, 0.35)), + ) + + # table + table: RigidObjectCfg = RigidObjectCfg( + prim_path="/World/envs/env_.*/table", + spawn=sim_utils.CuboidCfg( + size=(0.8, 1.5, 0.04), + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + collision_props=sim_utils.CollisionPropertiesCfg(), + # trick: we let visualizer's color to show the table with success coloring + visible=False, + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=(-0.55, 0.0, 0.235), rot=(1.0, 0.0, 0.0, 0.0)), + ) + + # plane + plane = AssetBaseCfg( + prim_path="/World/GroundPlane", + init_state=AssetBaseCfg.InitialStateCfg(), + spawn=sim_utils.GroundPlaneCfg(), + collision_group=-1, + ) + + # lights + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=750.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + +@configclass +class CommandsCfg: + """Command terms for the MDP.""" + + object_pose = mdp.ObjectUniformPoseCommandCfg( + asset_name="robot", + object_name="object", + resampling_time_range=(3.0, 5.0), + debug_vis=False, + ranges=mdp.ObjectUniformPoseCommandCfg.Ranges( + pos_x=(-0.7, -0.3), + pos_y=(-0.25, 0.25), + pos_z=(0.55, 0.95), + roll=(-3.14, 3.14), + pitch=(-3.14, 3.14), + yaw=(0.0, 0.0), + ), + success_vis_asset_name="table", + ) + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + object_quat_b = ObsTerm(func=mdp.object_quat_b, noise=Unoise(n_min=-0.0, n_max=0.0)) + target_object_pose_b = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"}) + actions = ObsTerm(func=mdp.last_action) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + self.history_length = 5 + + @configclass + class ProprioObsCfg(ObsGroup): + """Observations for proprioception group.""" + + joint_pos = ObsTerm(func=mdp.joint_pos, noise=Unoise(n_min=-0.0, n_max=0.0)) + joint_vel = ObsTerm(func=mdp.joint_vel, noise=Unoise(n_min=-0.0, n_max=0.0)) + hand_tips_state_b = ObsTerm( + func=mdp.body_state_b, + noise=Unoise(n_min=-0.0, n_max=0.0), + # good behaving number for position in m, velocity in m/s, rad/s, + # and quaternion are unlikely to exceed -2 to 2 range + clip=(-2.0, 2.0), + params={ + "body_asset_cfg": SceneEntityCfg("robot"), + "base_asset_cfg": SceneEntityCfg("robot"), + }, + ) + contact: ObsTerm = MISSING + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + self.history_length = 5 + + @configclass + class PerceptionObsCfg(ObsGroup): + + object_point_cloud = ObsTerm( + func=mdp.object_point_cloud_b, + noise=Unoise(n_min=-0.0, n_max=0.0), + clip=(-2.0, 2.0), # clamp between -2 m to 2 m + params={"num_points": 64, "flatten": True}, + ) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_dim = 0 + self.concatenate_terms = True + self.flatten_history_dim = True + self.history_length = 5 + + # observation groups + policy: PolicyCfg = PolicyCfg() + proprio: ProprioObsCfg = ProprioObsCfg() + perception: PerceptionObsCfg = PerceptionObsCfg() + + +@configclass +class EventCfg: + """Configuration for randomization.""" + + # -- pre-startup + randomize_object_scale = EventTerm( + func=mdp.randomize_rigid_body_scale, + mode="prestartup", + params={"scale_range": (0.75, 1.5), "asset_cfg": SceneEntityCfg("object")}, + ) + + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": [0.5, 1.0], + "dynamic_friction_range": [0.5, 1.0], + "restitution_range": [0.0, 0.0], + "num_buckets": 250, + }, + ) + + object_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("object", body_names=".*"), + "static_friction_range": [0.5, 1.0], + "dynamic_friction_range": [0.5, 1.0], + "restitution_range": [0.0, 0.0], + "num_buckets": 250, + }, + ) + + joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "stiffness_distribution_params": [0.5, 2.0], + "damping_distribution_params": [0.5, 2.0], + "operation": "scale", + }, + ) + + joint_friction = EventTerm( + func=mdp.randomize_joint_parameters, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "friction_distribution_params": [0.0, 5.0], + "operation": "scale", + }, + ) + + object_scale_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("object"), + "mass_distribution_params": [0.2, 2.0], + "operation": "scale", + }, + ) + + reset_table = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": {"x": [-0.05, 0.05], "y": [-0.05, 0.05], "z": [0.0, 0.0]}, + "velocity_range": {"x": [-0.0, 0.0], "y": [-0.0, 0.0], "z": [-0.0, 0.0]}, + "asset_cfg": SceneEntityCfg("table"), + }, + ) + + reset_object = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": { + "x": [-0.2, 0.2], + "y": [-0.2, 0.2], + "z": [0.0, 0.4], + "roll": [-3.14, 3.14], + "pitch": [-3.14, 3.14], + "yaw": [-3.14, 3.14], + }, + "velocity_range": {"x": [-0.0, 0.0], "y": [-0.0, 0.0], "z": [-0.0, 0.0]}, + "asset_cfg": SceneEntityCfg("object"), + }, + ) + + reset_root = EventTerm( + func=mdp.reset_root_state_uniform, + mode="reset", + params={ + "pose_range": {"x": [-0.0, 0.0], "y": [-0.0, 0.0], "yaw": [-0.0, 0.0]}, + "velocity_range": {"x": [-0.0, 0.0], "y": [-0.0, 0.0], "z": [-0.0, 0.0]}, + "asset_cfg": SceneEntityCfg("robot"), + }, + ) + + reset_robot_joints = EventTerm( + func=mdp.reset_joints_by_offset, + mode="reset", + params={ + "position_range": [-0.50, 0.50], + "velocity_range": [0.0, 0.0], + }, + ) + + reset_robot_wrist_joint = EventTerm( + func=mdp.reset_joints_by_offset, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names="iiwa7_joint_7"), + "position_range": [-3, 3], + "velocity_range": [0.0, 0.0], + }, + ) + + # Note (Octi): This is a deliberate trick in Remake to accelerate learning. + # By scheduling gravity as a curriculum — starting with no gravity (easy) + # and gradually introducing full gravity (hard) — the agent learns more smoothly. + # This removes the need for a special "Lift" reward (often required to push the + # agent to counter gravity), which has bonus effect of simplifying reward composition overall. + variable_gravity = EventTerm( + func=mdp.randomize_physics_scene_gravity, + mode="reset", + params={ + "gravity_distribution_params": ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), + "operation": "abs", + }, + ) + + +@configclass +class ActionsCfg: + pass + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + action_l2 = RewTerm(func=mdp.action_l2_clamped, weight=-0.005) + + action_rate_l2 = RewTerm(func=mdp.action_rate_l2_clamped, weight=-0.005) + + fingers_to_object = RewTerm(func=mdp.object_ee_distance, params={"std": 0.4}, weight=1.0) + + position_tracking = RewTerm( + func=mdp.position_command_error_tanh, + weight=2.0, + params={ + "asset_cfg": SceneEntityCfg("robot"), + "std": 0.2, + "command_name": "object_pose", + "align_asset_cfg": SceneEntityCfg("object"), + }, + ) + + orientation_tracking = RewTerm( + func=mdp.orientation_command_error_tanh, + weight=4.0, + params={ + "asset_cfg": SceneEntityCfg("robot"), + "std": 1.5, + "command_name": "object_pose", + "align_asset_cfg": SceneEntityCfg("object"), + }, + ) + + success = RewTerm( + func=mdp.success_reward, + weight=10, + params={ + "asset_cfg": SceneEntityCfg("robot"), + "pos_std": 0.1, + "rot_std": 0.5, + "command_name": "object_pose", + "align_asset_cfg": SceneEntityCfg("object"), + }, + ) + + early_termination = RewTerm(func=mdp.is_terminated_term, weight=-1, params={"term_keys": "abnormal_robot"}) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + object_out_of_bound = DoneTerm( + func=mdp.out_of_bound, + params={ + "in_bound_range": {"x": (-1.5, 0.5), "y": (-2.0, 2.0), "z": (0.0, 2.0)}, + "asset_cfg": SceneEntityCfg("object"), + }, + ) + + abnormal_robot = DoneTerm(func=mdp.abnormal_robot_state) + + +@configclass +class DexsuiteReorientEnvCfg(ManagerBasedEnvCfg): + """Dexsuite reorientation task definition, also the base definition for derivative Lift task and evaluation task""" + + # Scene settings + viewer: ViewerCfg = ViewerCfg(eye=(-2.25, 0.0, 0.75), lookat=(0.0, 0.0, 0.45), origin_type="env") + scene: SceneCfg = SceneCfg(num_envs=4096, env_spacing=3, replicate_physics=False) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + curriculum: CurriculumCfg | None = CurriculumCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 2 # 50 Hz + + # *single-goal setup + self.commands.object_pose.resampling_time_range = (10.0, 10.0) + self.commands.object_pose.position_only = False + self.commands.object_pose.success_visualizer_cfg.markers["failure"] = self.scene.table.spawn.replace( + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.25, 0.15, 0.15), roughness=0.25), visible=True + ) + self.commands.object_pose.success_visualizer_cfg.markers["success"] = self.scene.table.spawn.replace( + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.15, 0.25, 0.15), roughness=0.25), visible=True + ) + + self.episode_length_s = 4.0 + self.is_finite_horizon = True + + # simulation settings + self.sim.dt = 1 / 120 + self.sim.render_interval = self.decimation + self.sim.physx.bounce_threshold_velocity = 0.2 + self.sim.physx.bounce_threshold_velocity = 0.01 + self.sim.physx.gpu_max_rigid_patch_count = 4 * 5 * 2**15 + + if self.curriculum is not None: + self.curriculum.adr.params["pos_tol"] = self.rewards.success.params["pos_std"] / 2 + self.curriculum.adr.params["rot_tol"] = self.rewards.success.params["rot_std"] / 2 + + +class DexsuiteLiftEnvCfg(DexsuiteReorientEnvCfg): + """Dexsuite lift task definition""" + + def __post_init__(self): + super().__post_init__() + self.rewards.orientation_tracking = None # no orientation reward + self.commands.object_pose.position_only = True + if self.curriculum is not None: + self.rewards.success.params["rot_std"] = None # make success reward not consider orientation + self.curriculum.adr.params["rot_tol"] = None # make adr not tracking orientation + + +class DexsuiteReorientEnvCfg_PLAY(DexsuiteReorientEnvCfg): + """Dexsuite reorientation task evaluation environment definition""" + + def __post_init__(self): + super().__post_init__() + self.commands.object_pose.resampling_time_range = (2.0, 3.0) + self.commands.object_pose.debug_vis = True + self.curriculum.adr.params["init_difficulty"] = self.curriculum.adr.params["max_difficulty"] + + +class DexsuiteLiftEnvCfg_PLAY(DexsuiteLiftEnvCfg): + """Dexsuite lift task evaluation environment definition""" + + def __post_init__(self): + super().__post_init__() + self.commands.object_pose.resampling_time_range = (2.0, 3.0) + self.commands.object_pose.debug_vis = True + self.commands.object_pose.position_only = True + self.curriculum.adr.params["init_difficulty"] = self.curriculum.adr.params["max_difficulty"] diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py new file mode 100644 index 00000000000..794113f9253 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.envs.mdp import * # noqa: F401, F403 + +from .commands import * # noqa: F401, F403 +from .curriculums import * # noqa: F401, F403 +from .observations import * # noqa: F401, F403 +from .rewards import * # noqa: F401, F403 +from .terminations import * # noqa: F401, F403 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py new file mode 100644 index 00000000000..a5132558174 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from .pose_commands_cfg import * # noqa: F401, F403 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py new file mode 100644 index 00000000000..146eee9741d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands.py @@ -0,0 +1,179 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + + +"""Sub-module containing command generators for pose tracking.""" + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import CommandTerm +from isaaclab.markers import VisualizationMarkers +from isaaclab.utils.math import combine_frame_transforms, compute_pose_error, quat_from_euler_xyz, quat_unique + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedEnv + + from . import pose_commands_cfg as dex_cmd_cfgs + + +class ObjectUniformPoseCommand(CommandTerm): + """Uniform pose command generator for an object (in the robot base frame). + + This command term samples target object poses by: + • Drawing (x, y, z) uniformly within configured Cartesian bounds, and + • Drawing roll-pitch-yaw uniformly within configured ranges, then converting + to a quaternion (w, x, y, z). Optionally makes quaternions unique by enforcing + a positive real part. + + Frames: + Targets are defined in the robot's *base frame*. For metrics/visualization, + targets are transformed into the *world frame* using the robot root pose. + + Outputs: + The command buffer has shape (num_envs, 7): `(x, y, z, qw, qx, qy, qz)`. + + Metrics: + `position_error` and `orientation_error` are computed between the commanded + world-frame pose and the object's current world-frame pose. + + Config: + `cfg` must provide the sampling ranges, whether to enforce quaternion uniqueness, + and optional visualization settings. + """ + + cfg: dex_cmd_cfgs.ObjectUniformPoseCommandCfg + """Configuration for the command generator.""" + + def __init__(self, cfg: dex_cmd_cfgs.ObjectUniformPoseCommandCfg, env: ManagerBasedEnv): + """Initialize the command generator class. + + Args: + cfg: The configuration parameters for the command generator. + env: The environment object. + """ + # initialize the base class + super().__init__(cfg, env) + + # extract the robot and body index for which the command is generated + self.robot: Articulation = env.scene[cfg.asset_name] + self.object: RigidObject = env.scene[cfg.object_name] + self.success_vis_asset: RigidObject = env.scene[cfg.success_vis_asset_name] + + # create buffers + # -- commands: (x, y, z, qw, qx, qy, qz) in root frame + self.pose_command_b = torch.zeros(self.num_envs, 7, device=self.device) + self.pose_command_b[:, 3] = 1.0 + self.pose_command_w = torch.zeros_like(self.pose_command_b) + # -- metrics + self.metrics["position_error"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["orientation_error"] = torch.zeros(self.num_envs, device=self.device) + + self.success_visualizer = VisualizationMarkers(self.cfg.success_visualizer_cfg) + self.success_visualizer.set_visibility(True) + + def __str__(self) -> str: + msg = "UniformPoseCommand:\n" + msg += f"\tCommand dimension: {tuple(self.command.shape[1:])}\n" + msg += f"\tResampling time range: {self.cfg.resampling_time_range}\n" + return msg + + """ + Properties + """ + + @property + def command(self) -> torch.Tensor: + """The desired pose command. Shape is (num_envs, 7). + + The first three elements correspond to the position, followed by the quaternion orientation in (w, x, y, z). + """ + return self.pose_command_b + + """ + Implementation specific functions. + """ + + def _update_metrics(self): + # transform command from base frame to simulation world frame + self.pose_command_w[:, :3], self.pose_command_w[:, 3:] = combine_frame_transforms( + self.robot.data.root_pos_w, + self.robot.data.root_quat_w, + self.pose_command_b[:, :3], + self.pose_command_b[:, 3:], + ) + # compute the error + pos_error, rot_error = compute_pose_error( + self.pose_command_w[:, :3], + self.pose_command_w[:, 3:], + self.object.data.root_state_w[:, :3], + self.object.data.root_state_w[:, 3:7], + ) + self.metrics["position_error"] = torch.norm(pos_error, dim=-1) + self.metrics["orientation_error"] = torch.norm(rot_error, dim=-1) + + success_id = self.metrics["position_error"] < 0.05 + if not self.cfg.position_only: + success_id &= self.metrics["orientation_error"] < 0.5 + self.success_visualizer.visualize(self.success_vis_asset.data.root_pos_w, marker_indices=success_id.int()) + + def _resample_command(self, env_ids: Sequence[int]): + # sample new pose targets + # -- position + r = torch.empty(len(env_ids), device=self.device) + self.pose_command_b[env_ids, 0] = r.uniform_(*self.cfg.ranges.pos_x) + self.pose_command_b[env_ids, 1] = r.uniform_(*self.cfg.ranges.pos_y) + self.pose_command_b[env_ids, 2] = r.uniform_(*self.cfg.ranges.pos_z) + # -- orientation + euler_angles = torch.zeros_like(self.pose_command_b[env_ids, :3]) + euler_angles[:, 0].uniform_(*self.cfg.ranges.roll) + euler_angles[:, 1].uniform_(*self.cfg.ranges.pitch) + euler_angles[:, 2].uniform_(*self.cfg.ranges.yaw) + quat = quat_from_euler_xyz(euler_angles[:, 0], euler_angles[:, 1], euler_angles[:, 2]) + # make sure the quaternion has real part as positive + self.pose_command_b[env_ids, 3:] = quat_unique(quat) if self.cfg.make_quat_unique else quat + + def _update_command(self): + pass + + def _set_debug_vis_impl(self, debug_vis: bool): + # create markers if necessary for the first tome + if debug_vis: + if not hasattr(self, "goal_visualizer"): + # -- goal pose + self.goal_visualizer = VisualizationMarkers(self.cfg.goal_pose_visualizer_cfg) + # -- current body pose + self.curr_visualizer = VisualizationMarkers(self.cfg.curr_pose_visualizer_cfg) + # set their visibility to true + self.goal_visualizer.set_visibility(True) + self.curr_visualizer.set_visibility(True) + else: + if hasattr(self, "goal_visualizer"): + self.goal_visualizer.set_visibility(False) + self.curr_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # check if robot is initialized + # note: this is needed in-case the robot is de-initialized. we can't access the data + if not self.robot.is_initialized: + return + # update the markers + if not self.cfg.position_only: + # -- goal pose + self.goal_visualizer.visualize(self.pose_command_w[:, :3], self.pose_command_w[:, 3:]) + # -- current object pose + self.curr_visualizer.visualize(self.object.data.root_pos_w, self.object.data.root_quat_w) + else: + distance = torch.norm(self.pose_command_w[:, :3] - self.object.data.root_pos_w[:, :3], dim=1) + success_id = (distance < 0.05).int() + # note: since marker indices for position is 1(far) and 2(near), we can simply shift the success_id by 1. + # -- goal position + self.goal_visualizer.visualize(self.pose_command_w[:, :3], marker_indices=success_id + 1) + # -- current object position + self.curr_visualizer.visualize(self.object.data.root_pos_w, marker_indices=success_id + 1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py new file mode 100644 index 00000000000..8501c00116d --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/commands/pose_commands_cfg.py @@ -0,0 +1,92 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from dataclasses import MISSING + +import isaaclab.sim as sim_utils +from isaaclab.managers import CommandTermCfg +from isaaclab.markers import VisualizationMarkersCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from . import pose_commands as dex_cmd + +ALIGN_MARKER_CFG = VisualizationMarkersCfg( + markers={ + "frame": sim_utils.UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/UIElements/frame_prim.usd", + scale=(0.1, 0.1, 0.1), + ), + "position_far": sim_utils.SphereCfg( + radius=0.01, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)), + ), + "position_near": sim_utils.SphereCfg( + radius=0.01, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + ), + } +) + + +@configclass +class ObjectUniformPoseCommandCfg(CommandTermCfg): + """Configuration for uniform pose command generator.""" + + class_type: type = dex_cmd.ObjectUniformPoseCommand + + asset_name: str = MISSING + """Name of the coordinate referencing asset in the environment for which the commands are generated respect to.""" + + object_name: str = MISSING + """Name of the object in the environment for which the commands are generated.""" + + make_quat_unique: bool = False + """Whether to make the quaternion unique or not. Defaults to False. + + If True, the quaternion is made unique by ensuring the real part is positive. + """ + + @configclass + class Ranges: + """Uniform distribution ranges for the pose commands.""" + + pos_x: tuple[float, float] = MISSING + """Range for the x position (in m).""" + + pos_y: tuple[float, float] = MISSING + """Range for the y position (in m).""" + + pos_z: tuple[float, float] = MISSING + """Range for the z position (in m).""" + + roll: tuple[float, float] = MISSING + """Range for the roll angle (in rad).""" + + pitch: tuple[float, float] = MISSING + """Range for the pitch angle (in rad).""" + + yaw: tuple[float, float] = MISSING + """Range for the yaw angle (in rad).""" + + ranges: Ranges = MISSING + """Ranges for the commands.""" + + position_only: bool = True + """Command goal position only. Command includes goal quat if False""" + + # Pose Markers + goal_pose_visualizer_cfg: VisualizationMarkersCfg = ALIGN_MARKER_CFG.replace(prim_path="/Visuals/Command/goal_pose") + """The configuration for the goal pose visualization marker. Defaults to FRAME_MARKER_CFG.""" + + curr_pose_visualizer_cfg: VisualizationMarkersCfg = ALIGN_MARKER_CFG.replace(prim_path="/Visuals/Command/body_pose") + """The configuration for the current pose visualization marker. Defaults to FRAME_MARKER_CFG.""" + + success_vis_asset_name: str = MISSING + """Name of the asset in the environment for which the success color are indicated.""" + + # success markers + success_visualizer_cfg = VisualizationMarkersCfg(prim_path="/Visuals/SuccessMarkers", markers={}) + """The configuration for the success visualization marker. User needs to add the markers""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py new file mode 100644 index 00000000000..c1a8c0f0d66 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/curriculums.py @@ -0,0 +1,113 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from isaaclab.assets import Articulation, RigidObject +from isaaclab.envs import mdp +from isaaclab.managers import ManagerTermBase, SceneEntityCfg +from isaaclab.utils.math import combine_frame_transforms, compute_pose_error + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def initial_final_interpolate_fn(env: ManagerBasedRLEnv, env_id, data, initial_value, final_value, difficulty_term_str): + """ + Interpolate between initial value iv and final value fv, for any arbitrarily + nested structure of lists/tuples in 'data'. Scalars (int/float) are handled + at the leaves. + """ + # get the fraction scalar on the device + difficulty_term: DifficultyScheduler = getattr(env.curriculum_manager.cfg, difficulty_term_str).func + frac = difficulty_term.difficulty_frac + if frac < 0.1: + # no-op during start, since the difficulty fraction near 0 is wasting of resource. + return mdp.modify_env_param.NO_CHANGE + + # convert iv/fv to tensors, but we'll peel them apart in recursion + initial_value_tensor = torch.tensor(initial_value, device=env.device) + final_value_tensor = torch.tensor(final_value, device=env.device) + + return _recurse(initial_value_tensor.tolist(), final_value_tensor.tolist(), data, frac) + + +def _recurse(iv_elem, fv_elem, data_elem, frac): + # If it's a sequence, rebuild the same type with each element recursed + if isinstance(data_elem, Sequence) and not isinstance(data_elem, (str, bytes)): + # Note: we assume initial value element and final value element have the same structure as data + return type(data_elem)(_recurse(iv_e, fv_e, d_e, frac) for iv_e, fv_e, d_e in zip(iv_elem, fv_elem, data_elem)) + # Otherwise it's a leaf scalar: do the interpolation + new_val = frac * (fv_elem - iv_elem) + iv_elem + if isinstance(data_elem, int): + return int(new_val.item()) + else: + # cast floats or any numeric + return new_val.item() + + +class DifficultyScheduler(ManagerTermBase): + """Adaptive difficulty scheduler for curriculum learning. + + Tracks per-environment difficulty levels and adjusts them based on task performance. Difficulty increases when + position/orientation errors fall below given tolerances, and decreases otherwise (unless `promotion_only` is set). + The normalized average difficulty across environments is exposed as `difficulty_frac` for use in curriculum + interpolation. + + Args: + cfg: Configuration object specifying scheduler parameters. + env: The manager-based RL environment. + + """ + + def __init__(self, cfg, env): + super().__init__(cfg, env) + init_difficulty = self.cfg.params.get("init_difficulty", 0) + self.current_adr_difficulties = torch.ones(env.num_envs, device=env.device) * init_difficulty + self.difficulty_frac = 0 + + def get_state(self): + return self.current_adr_difficulties + + def set_state(self, state: torch.Tensor): + self.current_adr_difficulties = state.clone().to(self._env.device) + + def __call__( + self, + env: ManagerBasedRLEnv, + env_ids: Sequence[int], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), + pos_tol: float = 0.1, + rot_tol: float | None = None, + init_difficulty: int = 0, + min_difficulty: int = 0, + max_difficulty: int = 50, + promotion_only: bool = False, + ): + asset: Articulation = env.scene[asset_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + command = env.command_manager.get_command("object_pose") + des_pos_w, des_quat_w = combine_frame_transforms( + asset.data.root_pos_w[env_ids], asset.data.root_quat_w[env_ids], command[env_ids, :3], command[env_ids, 3:7] + ) + pos_err, rot_err = compute_pose_error( + des_pos_w, des_quat_w, object.data.root_pos_w[env_ids], object.data.root_quat_w[env_ids] + ) + pos_dist = torch.norm(pos_err, dim=1) + rot_dist = torch.norm(rot_err, dim=1) + move_up = (pos_dist < pos_tol) & (rot_dist < rot_tol) if rot_tol else pos_dist < pos_tol + demot = self.current_adr_difficulties[env_ids] if promotion_only else self.current_adr_difficulties[env_ids] - 1 + self.current_adr_difficulties[env_ids] = torch.where( + move_up, + self.current_adr_difficulties[env_ids] + 1, + demot, + ).clamp(min=min_difficulty, max=max_difficulty) + self.difficulty_frac = torch.mean(self.current_adr_difficulties) / max(max_difficulty, 1) + return self.difficulty_frac diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py new file mode 100644 index 00000000000..b48e4bcfb5c --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/observations.py @@ -0,0 +1,197 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import ManagerTermBase, SceneEntityCfg +from isaaclab.utils.math import quat_apply, quat_apply_inverse, quat_inv, quat_mul, subtract_frame_transforms + +from .utils import sample_object_point_cloud + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def object_pos_b( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +): + """Object position in the robot's root frame. + + Args: + env: The environment. + robot_cfg: Scene entity for the robot (reference frame). Defaults to ``SceneEntityCfg("robot")``. + object_cfg: Scene entity for the object. Defaults to ``SceneEntityCfg("object")``. + + Returns: + Tensor of shape ``(num_envs, 3)``: object position [x, y, z] expressed in the robot root frame. + """ + robot: RigidObject = env.scene[robot_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + return quat_apply_inverse(robot.data.root_quat_w, object.data.root_pos_w - robot.data.root_pos_w) + + +def object_quat_b( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), +) -> torch.Tensor: + """Object orientation in the robot's root frame. + + Args: + env: The environment. + robot_cfg: Scene entity for the robot (reference frame). Defaults to ``SceneEntityCfg("robot")``. + object_cfg: Scene entity for the object. Defaults to ``SceneEntityCfg("object")``. + + Returns: + Tensor of shape ``(num_envs, 4)``: object quaternion ``(w, x, y, z)`` in the robot root frame. + """ + robot: RigidObject = env.scene[robot_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + return quat_mul(quat_inv(robot.data.root_quat_w), object.data.root_quat_w) + + +def body_state_b( + env: ManagerBasedRLEnv, + body_asset_cfg: SceneEntityCfg, + base_asset_cfg: SceneEntityCfg, +) -> torch.Tensor: + """Body state (pos, quat, lin vel, ang vel) in the base asset's root frame. + + The state for each body is stacked horizontally as + ``[position(3), quaternion(4)(wxyz), linvel(3), angvel(3)]`` and then concatenated over bodies. + + Args: + env: The environment. + body_asset_cfg: Scene entity for the articulated body whose links are observed. + base_asset_cfg: Scene entity providing the reference (root) frame. + + Returns: + Tensor of shape ``(num_envs, num_bodies * 13)`` with per-body states expressed in the base root frame. + """ + body_asset: Articulation = env.scene[body_asset_cfg.name] + base_asset: Articulation = env.scene[base_asset_cfg.name] + # get world pose of bodies + body_pos_w = body_asset.data.body_pos_w[:, body_asset_cfg.body_ids].view(-1, 3) + body_quat_w = body_asset.data.body_quat_w[:, body_asset_cfg.body_ids].view(-1, 4) + body_lin_vel_w = body_asset.data.body_lin_vel_w[:, body_asset_cfg.body_ids].view(-1, 3) + body_ang_vel_w = body_asset.data.body_ang_vel_w[:, body_asset_cfg.body_ids].view(-1, 3) + num_bodies = int(body_pos_w.shape[0] / env.num_envs) + # get world pose of base frame + root_pos_w = base_asset.data.root_link_pos_w.unsqueeze(1).repeat_interleave(num_bodies, dim=1).view(-1, 3) + root_quat_w = base_asset.data.root_link_quat_w.unsqueeze(1).repeat_interleave(num_bodies, dim=1).view(-1, 4) + # transform from world body pose to local body pose + body_pos_b, body_quat_b = subtract_frame_transforms(root_pos_w, root_quat_w, body_pos_w, body_quat_w) + body_lin_vel_b = quat_apply_inverse(root_quat_w, body_lin_vel_w) + body_ang_vel_b = quat_apply_inverse(root_quat_w, body_ang_vel_w) + # concate and return + out = torch.cat((body_pos_b, body_quat_b, body_lin_vel_b, body_ang_vel_b), dim=1) + return out.view(env.num_envs, -1) + + +class object_point_cloud_b(ManagerTermBase): + """Object surface point cloud expressed in a reference asset's root frame. + + Points are pre-sampled on the object's surface in its local frame and transformed to world, + then into the reference (e.g., robot) root frame. Optionally visualizes the points. + + Args (from ``cfg.params``): + object_cfg: Scene entity for the object to sample. Defaults to ``SceneEntityCfg("object")``. + ref_asset_cfg: Scene entity providing the reference frame. Defaults to ``SceneEntityCfg("robot")``. + num_points: Number of points to sample on the object surface. Defaults to ``10``. + visualize: Whether to draw markers for the points. Defaults to ``True``. + static: If ``True``, cache world-space points on reset and reuse them (no per-step resampling). + + Returns (from ``__call__``): + If ``flatten=False``: tensor of shape ``(num_envs, num_points, 3)``. + If ``flatten=True``: tensor of shape ``(num_envs, 3 * num_points)``. + """ + + def __init__(self, cfg, env: ManagerBasedRLEnv): + super().__init__(cfg, env) + + self.object_cfg: SceneEntityCfg = cfg.params.get("object_cfg", SceneEntityCfg("object")) + self.ref_asset_cfg: SceneEntityCfg = cfg.params.get("ref_asset_cfg", SceneEntityCfg("robot")) + num_points: int = cfg.params.get("num_points", 10) + self.object: RigidObject = env.scene[self.object_cfg.name] + self.ref_asset: Articulation = env.scene[self.ref_asset_cfg.name] + # lazy initialize visualizer and point cloud + if cfg.params.get("visualize", True): + from isaaclab.markers import VisualizationMarkers + from isaaclab.markers.config import RAY_CASTER_MARKER_CFG + + ray_cfg = RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/ObservationPointCloud") + ray_cfg.markers["hit"].radius = 0.0025 + self.visualizer = VisualizationMarkers(ray_cfg) + self.points_local = sample_object_point_cloud( + env.num_envs, num_points, self.object.cfg.prim_path, device=env.device + ) + self.points_w = torch.zeros_like(self.points_local) + + def __call__( + self, + env: ManagerBasedRLEnv, + ref_asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), + num_points: int = 10, + flatten: bool = False, + visualize: bool = True, + ): + """Compute the object point cloud in the reference asset's root frame. + + Note: + Points are pre-sampled at initialization using ``self.num_points``; the ``num_points`` argument is + kept for API symmetry and does not change the sampled set at runtime. + + Args: + env: The environment. + ref_asset_cfg: Reference frame provider (root). Defaults to ``SceneEntityCfg("robot")``. + object_cfg: Object to sample. Defaults to ``SceneEntityCfg("object")``. + num_points: Unused at runtime; see note above. + flatten: If ``True``, return a flattened tensor ``(num_envs, 3 * num_points)``. + visualize: If ``True``, draw markers for the points. + + Returns: + Tensor of shape ``(num_envs, num_points, 3)`` or flattened if requested. + """ + ref_pos_w = self.ref_asset.data.root_pos_w.unsqueeze(1).repeat(1, num_points, 1) + ref_quat_w = self.ref_asset.data.root_quat_w.unsqueeze(1).repeat(1, num_points, 1) + + object_pos_w = self.object.data.root_pos_w.unsqueeze(1).repeat(1, num_points, 1) + object_quat_w = self.object.data.root_quat_w.unsqueeze(1).repeat(1, num_points, 1) + # apply rotation + translation + self.points_w = quat_apply(object_quat_w, self.points_local) + object_pos_w + if visualize: + self.visualizer.visualize(translations=self.points_w.view(-1, 3)) + object_point_cloud_pos_b, _ = subtract_frame_transforms(ref_pos_w, ref_quat_w, self.points_w, None) + + return object_point_cloud_pos_b.view(env.num_envs, -1) if flatten else object_point_cloud_pos_b + + +def fingers_contact_force_b( + env: ManagerBasedRLEnv, + contact_sensor_names: list[str], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +) -> torch.Tensor: + """base-frame contact forces from listed sensors, concatenated per env. + + Args: + env: The environment. + contact_sensor_names: Names of contact sensors in ``env.scene.sensors`` to read. + + Returns: + Tensor of shape ``(num_envs, 3 * num_sensors)`` with forces stacked horizontally as + ``[fx, fy, fz]`` per sensor. + """ + force_w = [env.scene.sensors[name].data.force_matrix_w.view(env.num_envs, 3) for name in contact_sensor_names] + force_w = torch.stack(force_w, dim=1) + robot: Articulation = env.scene[asset_cfg.name] + forces_b = quat_apply_inverse(robot.data.root_link_quat_w.unsqueeze(1).repeat(1, force_w.shape[1], 1), force_w) + return forces_b diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py new file mode 100644 index 00000000000..9a6170f1e4f --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/rewards.py @@ -0,0 +1,126 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +from isaaclab.assets import RigidObject +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import ContactSensor +from isaaclab.utils import math as math_utils +from isaaclab.utils.math import combine_frame_transforms, compute_pose_error + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def action_rate_l2_clamped(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize the rate of change of the actions using L2 squared kernel.""" + return torch.sum(torch.square(env.action_manager.action - env.action_manager.prev_action), dim=1).clamp(-1000, 1000) + + +def action_l2_clamped(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize the actions using L2 squared kernel.""" + return torch.sum(torch.square(env.action_manager.action), dim=1).clamp(-1000, 1000) + + +def object_ee_distance( + env: ManagerBasedRLEnv, + std: float, + object_cfg: SceneEntityCfg = SceneEntityCfg("object"), + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +) -> torch.Tensor: + """Reward reaching the object using a tanh-kernel on end-effector distance. + + The reward is close to 1 when the maximum distance between the object and any end-effector body is small. + """ + asset: RigidObject = env.scene[asset_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + asset_pos = asset.data.body_pos_w[:, asset_cfg.body_ids] + object_pos = object.data.root_pos_w + object_ee_distance = torch.norm(asset_pos - object_pos[:, None, :], dim=-1).max(dim=-1).values + return 1 - torch.tanh(object_ee_distance / std) + + +def contacts(env: ManagerBasedRLEnv, threshold: float) -> torch.Tensor: + """Penalize undesired contacts as the number of violations that are above a threshold.""" + + thumb_contact_sensor: ContactSensor = env.scene.sensors["thumb_link_3_object_s"] + index_contact_sensor: ContactSensor = env.scene.sensors["index_link_3_object_s"] + middle_contact_sensor: ContactSensor = env.scene.sensors["middle_link_3_object_s"] + ring_contact_sensor: ContactSensor = env.scene.sensors["ring_link_3_object_s"] + # check if contact force is above threshold + thumb_contact = thumb_contact_sensor.data.force_matrix_w.view(env.num_envs, 3) + index_contact = index_contact_sensor.data.force_matrix_w.view(env.num_envs, 3) + middle_contact = middle_contact_sensor.data.force_matrix_w.view(env.num_envs, 3) + ring_contact = ring_contact_sensor.data.force_matrix_w.view(env.num_envs, 3) + + thumb_contact_mag = torch.norm(thumb_contact, dim=-1) + index_contact_mag = torch.norm(index_contact, dim=-1) + middle_contact_mag = torch.norm(middle_contact, dim=-1) + ring_contact_mag = torch.norm(ring_contact, dim=-1) + good_contact_cond1 = (thumb_contact_mag > threshold) & ( + (index_contact_mag > threshold) | (middle_contact_mag > threshold) | (ring_contact_mag > threshold) + ) + + return good_contact_cond1 + + +def success_reward( + env: ManagerBasedRLEnv, + command_name: str, + asset_cfg: SceneEntityCfg, + align_asset_cfg: SceneEntityCfg, + pos_std: float, + rot_std: float | None = None, +) -> torch.Tensor: + """Reward success by comparing commanded pose to the object pose using tanh kernels on error.""" + + asset: RigidObject = env.scene[asset_cfg.name] + object: RigidObject = env.scene[align_asset_cfg.name] + command = env.command_manager.get_command(command_name) + des_pos_w, des_quat_w = combine_frame_transforms( + asset.data.root_pos_w, asset.data.root_quat_w, command[:, :3], command[:, 3:7] + ) + pos_err, rot_err = compute_pose_error(des_pos_w, des_quat_w, object.data.root_pos_w, object.data.root_quat_w) + pos_dist = torch.norm(pos_err, dim=1) + if not rot_std: + # square is not necessary but this help to keep the final value between having rot_std or not roughly the same + return (1 - torch.tanh(pos_dist / pos_std)) ** 2 + rot_dist = torch.norm(rot_err, dim=1) + return (1 - torch.tanh(pos_dist / pos_std)) * (1 - torch.tanh(rot_dist / rot_std)) + + +def position_command_error_tanh( + env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg, align_asset_cfg: SceneEntityCfg +) -> torch.Tensor: + """Reward tracking of commanded position using tanh kernel, gated by contact presence.""" + + asset: RigidObject = env.scene[asset_cfg.name] + object: RigidObject = env.scene[align_asset_cfg.name] + command = env.command_manager.get_command(command_name) + # obtain the desired and current positions + des_pos_b = command[:, :3] + des_pos_w, _ = combine_frame_transforms(asset.data.root_pos_w, asset.data.root_quat_w, des_pos_b) + distance = torch.norm(object.data.root_pos_w - des_pos_w, dim=1) + return (1 - torch.tanh(distance / std)) * contacts(env, 1.0).float() + + +def orientation_command_error_tanh( + env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg, align_asset_cfg: SceneEntityCfg +) -> torch.Tensor: + """Reward tracking of commanded orientation using tanh kernel, gated by contact presence.""" + + asset: RigidObject = env.scene[asset_cfg.name] + object: RigidObject = env.scene[align_asset_cfg.name] + command = env.command_manager.get_command(command_name) + # obtain the desired and current orientations + des_quat_b = command[:, 3:7] + des_quat_w = math_utils.quat_mul(asset.data.root_state_w[:, 3:7], des_quat_b) + quat_distance = math_utils.quat_error_magnitude(object.data.root_quat_w, des_quat_w) + + return (1 - torch.tanh(quat_distance / std)) * contacts(env, 1.0).float() diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py new file mode 100644 index 00000000000..3ef9cf14b0a --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/terminations.py @@ -0,0 +1,49 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Common functions that can be used to activate certain terminations for the dexsuite task. + +The functions can be passed to the :class:`isaaclab.managers.TerminationTermCfg` object to enable +the termination introduced by the function. +""" + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def out_of_bound( + env: ManagerBasedRLEnv, + asset_cfg: SceneEntityCfg = SceneEntityCfg("object"), + in_bound_range: dict[str, tuple[float, float]] = {}, +) -> torch.Tensor: + """Termination condition for the object falls out of bound. + + Args: + env: The environment. + asset_cfg: The object configuration. Defaults to SceneEntityCfg("object"). + in_bound_range: The range in x, y, z such that the object is considered in range + """ + object: RigidObject = env.scene[asset_cfg.name] + range_list = [in_bound_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]] + ranges = torch.tensor(range_list, device=env.device) + + object_pos_local = object.data.root_pos_w - env.scene.env_origins + outside_bounds = ((object_pos_local < ranges[:, 0]) | (object_pos_local > ranges[:, 1])).any(dim=1) + return outside_bounds + + +def abnormal_robot_state(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Terminating environment when violation of velocity limits detects, this usually indicates unstable physics caused + by very bad, or aggressive action""" + robot: Articulation = env.scene[asset_cfg.name] + return (robot.data.joint_vel.abs() > (robot.data.joint_vel_limits * 2)).any(dim=1) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py new file mode 100644 index 00000000000..f7b8e9db59b --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/mdp/utils.py @@ -0,0 +1,247 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import hashlib +import logging +import numpy as np +import torch +import trimesh +from trimesh.sample import sample_surface + +import isaacsim.core.utils.prims as prim_utils +from pxr import UsdGeom + +from isaaclab.sim.utils import get_all_matching_child_prims + +# ---- module-scope caches ---- +_PRIM_SAMPLE_CACHE: dict[tuple[str, int], np.ndarray] = {} # (prim_hash, num_points) -> (N,3) in root frame +_FINAL_SAMPLE_CACHE: dict[str, np.ndarray] = {} # env_hash -> (num_points,3) in root frame + + +def clear_pointcloud_caches(): + _PRIM_SAMPLE_CACHE.clear() + _FINAL_SAMPLE_CACHE.clear() + + +def sample_object_point_cloud(num_envs: int, num_points: int, prim_path: str, device: str = "cpu") -> torch.Tensor: + """ + Samples point clouds for each environment instance by collecting points + from all matching USD prims under `prim_path`, then downsamples to + exactly `num_points` per env using farthest-point sampling. + + Caching is in-memory within this module: + - per-prim raw samples: _PRIM_SAMPLE_CACHE[(prim_hash, num_points)] + - final downsampled env: _FINAL_SAMPLE_CACHE[env_hash] + + Returns: + torch.Tensor: Shape (num_envs, num_points, 3) on `device`. + """ + points = torch.zeros((num_envs, num_points, 3), dtype=torch.float32, device=device) + xform_cache = UsdGeom.XformCache() + + for i in range(num_envs): + # Resolve prim path + obj_path = prim_path.replace(".*", str(i)) + + # Gather prims + prims = get_all_matching_child_prims( + obj_path, predicate=lambda p: p.GetTypeName() in ("Mesh", "Cube", "Sphere", "Cylinder", "Capsule", "Cone") + ) + if not prims: + raise KeyError(f"No valid prims under {obj_path}") + + object_prim = prim_utils.get_prim_at_path(obj_path) + world_root = xform_cache.GetLocalToWorldTransform(object_prim) + + # hash each child prim by its rel transform + geometry + prim_hashes = [] + for prim in prims: + prim_type = prim.GetTypeName() + hasher = hashlib.sha256() + + rel = world_root.GetInverse() * xform_cache.GetLocalToWorldTransform(prim) # prim -> root + mat_np = np.array([[rel[r][c] for c in range(4)] for r in range(4)], dtype=np.float32) + hasher.update(mat_np.tobytes()) + + if prim_type == "Mesh": + mesh = UsdGeom.Mesh(prim) + verts = np.asarray(mesh.GetPointsAttr().Get(), dtype=np.float32) + hasher.update(verts.tobytes()) + else: + if prim_type == "Cube": + size = UsdGeom.Cube(prim).GetSizeAttr().Get() + hasher.update(np.float32(size).tobytes()) + elif prim_type == "Sphere": + r = UsdGeom.Sphere(prim).GetRadiusAttr().Get() + hasher.update(np.float32(r).tobytes()) + elif prim_type == "Cylinder": + c = UsdGeom.Cylinder(prim) + hasher.update(np.float32(c.GetRadiusAttr().Get()).tobytes()) + hasher.update(np.float32(c.GetHeightAttr().Get()).tobytes()) + elif prim_type == "Capsule": + c = UsdGeom.Capsule(prim) + hasher.update(np.float32(c.GetRadiusAttr().Get()).tobytes()) + hasher.update(np.float32(c.GetHeightAttr().Get()).tobytes()) + elif prim_type == "Cone": + c = UsdGeom.Cone(prim) + hasher.update(np.float32(c.GetRadiusAttr().Get()).tobytes()) + hasher.update(np.float32(c.GetHeightAttr().Get()).tobytes()) + + prim_hashes.append(hasher.hexdigest()) + + # scale on root (default to 1 if missing) + attr = object_prim.GetAttribute("xformOp:scale") + scale_val = attr.Get() if attr else None + if scale_val is None: + base_scale = torch.ones(3, dtype=torch.float32, device=device) + else: + base_scale = torch.tensor(scale_val, dtype=torch.float32, device=device) + + # env-level cache key (includes num_points) + env_key = "_".join(sorted(prim_hashes)) + f"_{num_points}" + env_hash = hashlib.sha256(env_key.encode()).hexdigest() + + # load from env-level in-memory cache + if env_hash in _FINAL_SAMPLE_CACHE: + arr = _FINAL_SAMPLE_CACHE[env_hash] # (num_points,3) in root frame + points[i] = torch.from_numpy(arr).to(device) * base_scale.unsqueeze(0) + continue + + # otherwise build per-prim samples (with per-prim cache) + all_samples_np: list[np.ndarray] = [] + for prim, ph in zip(prims, prim_hashes): + key = (ph, num_points) + if key in _PRIM_SAMPLE_CACHE: + samples = _PRIM_SAMPLE_CACHE[key] + else: + prim_type = prim.GetTypeName() + if prim_type == "Mesh": + mesh = UsdGeom.Mesh(prim) + verts = np.asarray(mesh.GetPointsAttr().Get(), dtype=np.float32) + faces = _triangulate_faces(prim) + mesh_tm = trimesh.Trimesh(vertices=verts, faces=faces, process=False) + else: + mesh_tm = create_primitive_mesh(prim) + + face_weights = mesh_tm.area_faces + samples_np, _ = sample_surface(mesh_tm, num_points * 2, face_weight=face_weights) + + # FPS to num_points on chosen device + tensor_pts = torch.from_numpy(samples_np.astype(np.float32)).to(device) + prim_idxs = farthest_point_sampling(tensor_pts, num_points) + local_pts = tensor_pts[prim_idxs] + + # prim -> root transform + rel = xform_cache.GetLocalToWorldTransform(prim) * world_root.GetInverse() + mat_np = np.array([[rel[r][c] for c in range(4)] for r in range(4)], dtype=np.float32) + mat_t = torch.from_numpy(mat_np).to(device) + + ones = torch.ones((num_points, 1), device=device) + pts_h = torch.cat([local_pts, ones], dim=1) + root_h = pts_h @ mat_t + samples = root_h[:, :3].detach().cpu().numpy() + + if prim_type == "Cone": + samples[:, 2] -= UsdGeom.Cone(prim).GetHeightAttr().Get() / 2 + + _PRIM_SAMPLE_CACHE[key] = samples # cache in root frame @ num_points + + all_samples_np.append(samples) + + # combine & env-level FPS (if needed) + if len(all_samples_np) == 1: + samples_final = torch.from_numpy(all_samples_np[0]).to(device) + else: + combined = torch.from_numpy(np.concatenate(all_samples_np, axis=0)).to(device) + idxs = farthest_point_sampling(combined, num_points) + samples_final = combined[idxs] + + # store env-level cache in root frame (CPU) + _FINAL_SAMPLE_CACHE[env_hash] = samples_final.detach().cpu().numpy() + + # apply root scale and write out + points[i] = samples_final * base_scale.unsqueeze(0) + + return points + + +def _triangulate_faces(prim) -> np.ndarray: + """Convert a USD Mesh prim into triangulated face indices (N, 3).""" + mesh = UsdGeom.Mesh(prim) + counts = mesh.GetFaceVertexCountsAttr().Get() + indices = mesh.GetFaceVertexIndicesAttr().Get() + faces = [] + it = iter(indices) + for cnt in counts: + poly = [next(it) for _ in range(cnt)] + for k in range(1, cnt - 1): + faces.append([poly[0], poly[k], poly[k + 1]]) + return np.asarray(faces, dtype=np.int64) + + +def create_primitive_mesh(prim) -> trimesh.Trimesh: + """Create a trimesh mesh from a USD primitive (Cube, Sphere, Cylinder, etc.).""" + prim_type = prim.GetTypeName() + if prim_type == "Cube": + size = UsdGeom.Cube(prim).GetSizeAttr().Get() + return trimesh.creation.box(extents=(size, size, size)) + elif prim_type == "Sphere": + r = UsdGeom.Sphere(prim).GetRadiusAttr().Get() + return trimesh.creation.icosphere(subdivisions=3, radius=r) + elif prim_type == "Cylinder": + c = UsdGeom.Cylinder(prim) + return trimesh.creation.cylinder(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) + elif prim_type == "Capsule": + c = UsdGeom.Capsule(prim) + return trimesh.creation.capsule(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) + elif prim_type == "Cone": # Cone + c = UsdGeom.Cone(prim) + return trimesh.creation.cone(radius=c.GetRadiusAttr().Get(), height=c.GetHeightAttr().Get()) + else: + raise KeyError(f"{prim_type} is not a valid primitive mesh type") + + +def farthest_point_sampling( + points: torch.Tensor, n_samples: int, memory_threashold=2 * 1024**3 +) -> torch.Tensor: # 2 GiB + """ + Farthest Point Sampling (FPS) for point sets. + + Selects `n_samples` points such that each new point is farthest from the + already chosen ones. Uses a full pairwise distance matrix if memory allows, + otherwise falls back to an iterative version. + + Args: + points (torch.Tensor): Input points of shape (N, D). + n_samples (int): Number of samples to select. + memory_threashold (int): Max allowed bytes for distance matrix. Default 2 GiB. + + Returns: + torch.Tensor: Indices of sampled points (n_samples,). + """ + device = points.device + N = points.shape[0] + elem_size = points.element_size() + bytes_needed = N * N * elem_size + if bytes_needed <= memory_threashold: + dist_mat = torch.cdist(points, points) + sampled_idx = torch.zeros(n_samples, dtype=torch.long, device=device) + min_dists = torch.full((N,), float("inf"), device=device) + farthest = torch.randint(0, N, (1,), device=device) + for j in range(n_samples): + sampled_idx[j] = farthest + min_dists = torch.minimum(min_dists, dist_mat[farthest].view(-1)) + farthest = torch.argmax(min_dists) + return sampled_idx + logging.warning(f"FPS fallback to iterative (needed {bytes_needed} > {memory_threashold})") + sampled_idx = torch.zeros(n_samples, dtype=torch.long, device=device) + distances = torch.full((N,), float("inf"), device=device) + farthest = torch.randint(0, N, (1,), device=device) + for j in range(n_samples): + sampled_idx[j] = farthest + dist = torch.norm(points - points[farthest], dim=1) + distances = torch.minimum(distances, dist) + farthest = torch.argmax(distances) + return sampled_idx From 40c8d16db36496a4b82c3e5f3ce65005a56c2e65 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Tue, 9 Sep 2025 16:29:18 -0700 Subject: [PATCH 10/50] Adds PBT algorithm to rl games (#3399) # Description This PR introduces the Population Based Training algorithm originally implemented in Petrenko, Aleksei, et al. "Dexpbt: Scaling up dexterous manipulation for hand-arm systems with population based training." arXiv preprint arXiv:2305.12127 (2023). Pbt algorithm offers a alternative to scaling when increasing number of environment has margin effect. It takes idea in natural selection and stochastic property in rl-training to always keeps the top performing agent while replace weak agent with top performance to overcome the catastrophic failure, and improve the exploration. Training view, underperformers are rescued by best performers and later surpasses them and become best performers Screenshot from 2025-09-09 00-55-11 Note: PBT is still at beta phase and has below limitations: 1. in theory It can work with any rl algorithm but current implementation only works for rl-games 2. The API could be furthur simplified without needing explicitly input num_policies or policy_idx, which allows for dynamic max_population, but it is for future work ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- docs/index.rst | 1 + .../features/population_based_training.rst | 112 +++++++ .../reinforcement_learning/rl_games/train.py | 17 +- source/isaaclab_rl/config/extension.toml | 2 +- source/isaaclab_rl/docs/CHANGELOG.rst | 9 + .../isaaclab_rl/rl_games/__init__.py | 9 + .../isaaclab_rl/rl_games/pbt/__init__.py | 7 + .../isaaclab_rl/rl_games/pbt/mutation.py | 48 +++ .../isaaclab_rl/rl_games/pbt/pbt.py | 260 +++++++++++++++ .../isaaclab_rl/rl_games/pbt/pbt_cfg.py | 63 ++++ .../isaaclab_rl/rl_games/pbt/pbt_utils.py | 295 ++++++++++++++++++ .../isaaclab_rl/{ => rl_games}/rl_games.py | 0 12 files changed, 819 insertions(+), 4 deletions(-) create mode 100644 docs/source/features/population_based_training.rst create mode 100644 source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py create mode 100644 source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py create mode 100644 source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py create mode 100644 source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py create mode 100644 source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py create mode 100644 source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py rename source/isaaclab_rl/isaaclab_rl/{ => rl_games}/rl_games.py (100%) diff --git a/docs/index.rst b/docs/index.rst index baeeffdd35f..7d1e4cbc8c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -116,6 +116,7 @@ Table of Contents source/features/hydra source/features/multi_gpu + source/features/population_based_training Tiled Rendering source/features/ray source/features/reproducibility diff --git a/docs/source/features/population_based_training.rst b/docs/source/features/population_based_training.rst new file mode 100644 index 00000000000..b2a74d96457 --- /dev/null +++ b/docs/source/features/population_based_training.rst @@ -0,0 +1,112 @@ +Population Based Training +========================= + +What PBT Does +------------- + +* Trains *N* policies in parallel (a "population") on the **same task**. +* Every ``interval_steps``: + + #. Save each policy's checkpoint and objective. + #. Score the population and identify **leaders** and **underperformers**. + #. For underperformers, replace weights from a random leader and **mutate** selected hyperparameters. + #. Restart that process with the new weights/params automatically. + +Leader / Underperformer Selection +--------------------------------- + +Let ``o_i`` be each initialized policy's objective, with mean ``μ`` and std ``σ``. + +Upper and lower performance cuts are:: + + upper_cut = max(μ + threshold_std * σ, μ + threshold_abs) + lower_cut = min(μ - threshold_std * σ, μ - threshold_abs) + +* **Leaders**: ``o_i > upper_cut`` +* **Underperformers**: ``o_i < lower_cut`` + +The "Natural-Selection" rules: + +1. Only underperformers are acted on (mutated or replaced). +2. If leaders exist, replace an underperformer with a random leader; otherwise, self-mutate. + +Mutation (Hyperparameters) +-------------------------- + +* Each param has a mutation function (e.g., ``mutate_float``, ``mutate_discount``, etc.). +* A param is mutated with probability ``mutation_rate``. +* When mutated, its value is perturbed within ``change_range = (min, max)``. +* Only whitelisted keys (from the PBT config) are considered. + +Example Config +-------------- + +.. code-block:: yaml + + pbt: + enabled: True + policy_idx: 0 + num_policies: 8 + directory: . + workspace: "pbt_workspace" + objective: Curriculum/difficulty_level + interval_steps: 50000000 + threshold_std: 0.1 + threshold_abs: 0.025 + mutation_rate: 0.25 + change_range: [1.1, 2.0] + mutation: + agent.params.config.learning_rate: "mutate_float" + agent.params.config.grad_norm: "mutate_float" + agent.params.config.entropy_coef: "mutate_float" + agent.params.config.critic_coef: "mutate_float" + agent.params.config.bounds_loss_coef: "mutate_float" + agent.params.config.kl_threshold: "mutate_float" + agent.params.config.gamma: "mutate_discount" + agent.params.config.tau: "mutate_discount" + + +``objective: Curriculum/difficulty_level`` uses ``infos["episode"]["Curriculum/difficulty_level"]`` as the scalar to +**rank policies** (higher is better). With ``num_policies: 8``, launch eight processes sharing the same ``workspace`` +and unique ``policy_idx`` (0-7). + + +Launching PBT +------------- + +You must start **one process per policy** and point them to the **same workspace**. Set a unique +``policy_idx`` for each process and the common ``num_policies``. + +Minimal flags you need: + +* ``agent.pbt.enabled=True`` +* ``agent.pbt.workspace=`` +* ``agent.pbt.policy_idx=<0..num_policies-1>`` +* ``agent.pbt.num_policies=`` + +.. note:: + All processes must use the same ``agent.pbt.workspace`` so they can see each other's checkpoints. + +.. caution:: + PBT is currently supported **only** with the **rl_games** library. Other RL libraries are not supported yet. + +Tips +---- + +* Keep checkpoints fast: reduce ``interval_steps`` only if you really need tighter PBT cadence. +* It is recommended to run 6+ workers to see benefit of pbt + + +References +---------- + +This PBT implementation reimplements and is inspired by *Dexpbt: Scaling up dexterous manipulation for hand-arm systems with population based training* (Petrenko et al., 2023). + +.. code-block:: bibtex + + @article{petrenko2023dexpbt, + title={Dexpbt: Scaling up dexterous manipulation for hand-arm systems with population based training}, + author={Petrenko, Aleksei and Allshire, Arthur and State, Gavriel and Handa, Ankur and Makoviychuk, Viktor}, + journal={arXiv preprint arXiv:2305.12127}, + year={2023} + } diff --git a/scripts/reinforcement_learning/rl_games/train.py b/scripts/reinforcement_learning/rl_games/train.py index cc1e54b1756..711682f83e4 100644 --- a/scripts/reinforcement_learning/rl_games/train.py +++ b/scripts/reinforcement_learning/rl_games/train.py @@ -81,7 +81,7 @@ from isaaclab.utils.dict import print_dict from isaaclab.utils.io import dump_pickle, dump_yaml -from isaaclab_rl.rl_games import RlGamesGpuEnv, RlGamesVecEnvWrapper +from isaaclab_rl.rl_games import MultiObserver, PbtAlgoObserver, RlGamesGpuEnv, RlGamesVecEnvWrapper import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.hydra import hydra_task_config @@ -127,7 +127,12 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # specify directory for logging experiments config_name = agent_cfg["params"]["config"]["name"] log_root_path = os.path.join("logs", "rl_games", config_name) - log_root_path = os.path.abspath(log_root_path) + if "pbt" in agent_cfg: + if agent_cfg["pbt"]["directory"] == ".": + log_root_path = os.path.abspath(log_root_path) + else: + log_root_path = os.path.join(agent_cfg["pbt"]["directory"], log_root_path) + print(f"[INFO] Logging experiment in directory: {log_root_path}") # specify directory for logging runs log_dir = agent_cfg["params"]["config"].get("full_experiment_name", datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) @@ -192,7 +197,13 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # set number of actors into agent config agent_cfg["params"]["config"]["num_actors"] = env.unwrapped.num_envs # create runner from rl-games - runner = Runner(IsaacAlgoObserver()) + + if "pbt" in agent_cfg and agent_cfg["pbt"]["enabled"]: + observers = MultiObserver([IsaacAlgoObserver(), PbtAlgoObserver(agent_cfg, args_cli)]) + runner = Runner(observers) + else: + runner = Runner(IsaacAlgoObserver()) + runner.load(agent_cfg) # reset the agent and env diff --git a/source/isaaclab_rl/config/extension.toml b/source/isaaclab_rl/config/extension.toml index 26a2675f922..3539dcbacc2 100644 --- a/source/isaaclab_rl/config/extension.toml +++ b/source/isaaclab_rl/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.3.0" +version = "0.4.0" # Description title = "Isaac Lab RL" diff --git a/source/isaaclab_rl/docs/CHANGELOG.rst b/source/isaaclab_rl/docs/CHANGELOG.rst index d0252ca0dba..98e1115d9ba 100644 --- a/source/isaaclab_rl/docs/CHANGELOG.rst +++ b/source/isaaclab_rl/docs/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog --------- +0.4.0 (2025-09-09) +~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Introduced PBT to rl-games. + + 0.3.0 (2025-09-03) ~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py b/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py new file mode 100644 index 00000000000..38bfa1f4ec3 --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Wrappers and utilities to configure an environment for rl-games library.""" + +from .pbt import * +from .rl_games import * diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py new file mode 100644 index 00000000000..5eab19288f0 --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from .pbt import MultiObserver, PbtAlgoObserver +from .pbt_cfg import PbtCfg diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py new file mode 100644 index 00000000000..bd6f04be093 --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/mutation.py @@ -0,0 +1,48 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import random +from collections.abc import Callable +from typing import Any + + +def mutate_float(x: float, change_min: float = 1.1, change_max: float = 1.5) -> float: + """Multiply or divide by a random factor in [change_min, change_max].""" + k = random.uniform(change_min, change_max) + return x / k if random.random() < 0.5 else x * k + + +def mutate_discount(x: float, **kwargs) -> float: + """Conservative change near 1.0 by mutating (1 - x) in [1.1, 1.2].""" + inv = 1.0 - x + new_inv = mutate_float(inv, change_min=1.1, change_max=1.2) + return 1.0 - new_inv + + +MUTATION_FUNCS: dict[str, Callable[..., Any]] = { + "mutate_float": mutate_float, + "mutate_discount": mutate_discount, +} + + +def mutate( + params: dict[str, Any], + mutations: dict[str, str], + mutation_rate: float, + change_range: tuple[float, float], +) -> dict[str, Any]: + cmin, cmax = change_range + out: dict[str, Any] = {} + for name, val in params.items(): + fn_name = mutations.get(name) + # skip if no rule or coin flip says "no" + if fn_name is None or random.random() > mutation_rate: + out[name] = val + continue + fn = MUTATION_FUNCS.get(fn_name) + if fn is None: + raise KeyError(f"Unknown mutation function: {fn_name!r}") + out[name] = fn(val, change_min=cmin, change_max=cmax) + return out diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py new file mode 100644 index 00000000000..dbff143d19c --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py @@ -0,0 +1,260 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np +import os +import random +import sys +import torch +import torch.distributed as dist + +from rl_games.common.algo_observer import AlgoObserver + +from . import pbt_utils +from .mutation import mutate +from .pbt_cfg import PbtCfg + +# i.e. value for target objective when it is not known +_UNINITIALIZED_VALUE = float(-1e9) + + +class PbtAlgoObserver(AlgoObserver): + """rl_games observer that implements Population-Based Training for a single policy process.""" + + def __init__(self, params, args_cli): + """Initialize observer, print the mutation table, and allocate the restart flag. + + Args: + params (dict): Full agent/task params (Hydra style). + args_cli: Parsed CLI args used to reconstruct a restart command. + """ + super().__init__() + self.printer = pbt_utils.PbtTablePrinter() + self.dir = params["pbt"]["directory"] + + self.rendering_args = pbt_utils.RenderingArgs(args_cli) + self.wandb_args = pbt_utils.WandbArgs(args_cli) + self.env_args = pbt_utils.EnvArgs(args_cli) + self.distributed_args = pbt_utils.DistributedArgs(args_cli) + self.cfg = PbtCfg(**params["pbt"]) + self.pbt_it = -1 # dummy value, stands for "not initialized" + self.score = _UNINITIALIZED_VALUE + self.pbt_params = pbt_utils.filter_params(pbt_utils.flatten_dict({"agent": params}), self.cfg.mutation) + + assert len(self.pbt_params) > 0, "[DANGER]: Dictionary that contains params to mutate is empty" + self.printer.print_params_table(self.pbt_params, header="List of params to mutate") + + self.device = params["params"]["config"]["device"] + self.restart_flag = torch.tensor([0], device=self.device) + + def after_init(self, algo): + """Capture training directories on rank 0 and create this policy's workspace folder. + + Args: + algo: rl_games algorithm object (provides writer, train_dir, frame counter, etc.). + """ + if self.distributed_args.rank != 0: + return + + self.algo = algo + self.root_dir = algo.train_dir + self.ws_dir = os.path.join(self.root_dir, self.cfg.workspace) + self.curr_policy_dir = os.path.join(self.ws_dir, f"{self.cfg.policy_idx:03d}") + os.makedirs(self.curr_policy_dir, exist_ok=True) + + def process_infos(self, infos, done_indices): + """Extract the scalar objective from environment infos and store in `self.score`. + + Notes: + Expects the objective to be at `infos["episode"][self.cfg.objective]`. + """ + self.score = infos["episode"][self.cfg.objective] + + def after_steps(self): + """Main PBT tick executed every train step. + + Flow: + 1) Non-zero ranks: exit immediately if `restart_flag == 1`, else return. + 2) Rank 0: if `restart_flag == 1`, restart this process with new params. + 3) Rank 0: on PBT cadence boundary (`interval_steps`), save checkpoint, + load population checkpoints, compute bands, and if this policy is an + underperformer, select a replacement (random leader or self), mutate + whitelisted params, set `restart_flag`, broadcast (if distributed), + and print a mutation diff table. + """ + if self.distributed_args.rank != 0: + if self.restart_flag.cpu().item() == 1: + os._exit(0) + return + + elif self.restart_flag.cpu().item() == 1: + self._restart_with_new_params(self.new_params, self.restart_from_checkpoint) + return + + # Non-zero can continue + if self.distributed_args.rank != 0: + return + + if self.pbt_it == -1: + self.pbt_it = self.algo.frame // self.cfg.interval_steps + return + + if self.algo.frame // self.cfg.interval_steps <= self.pbt_it: + return + + self.pbt_it = self.algo.frame // self.cfg.interval_steps + frame_left = (self.pbt_it + 1) * self.cfg.interval_steps - self.algo.frame + print(f"Policy {self.cfg.policy_idx}, frames_left {frame_left}, PBT it {self.pbt_it}") + try: + pbt_utils.save_pbt_checkpoint(self.curr_policy_dir, self.score, self.pbt_it, self.algo, self.pbt_params) + ckpts = pbt_utils.load_pbt_ckpts(self.ws_dir, self.cfg.policy_idx, self.cfg.num_policies, self.pbt_it) + pbt_utils.cleanup(ckpts, self.curr_policy_dir) + except Exception as exc: + print(f"Policy {self.cfg.policy_idx}: Exception {exc} during sanity log!") + return + + sumry = {i: None if c is None else {k: v for k, v in c.items() if k != "params"} for i, c in ckpts.items()} + self.printer.print_ckpt_summary(sumry) + + policies = list(range(self.cfg.num_policies)) + target_objectives = [ckpts[p]["true_objective"] if ckpts[p] else _UNINITIALIZED_VALUE for p in policies] + initialized = [(obj, p) for obj, p in zip(target_objectives, policies) if obj > _UNINITIALIZED_VALUE] + if not initialized: + print("No policies initialized; skipping PBT iteration.") + return + initialized_objectives, initialized_policies = zip(*initialized) + + # 1) Stats + mean_obj = float(np.mean(initialized_objectives)) + std_obj = float(np.std(initialized_objectives)) + upper_cut = max(mean_obj + self.cfg.threshold_std * std_obj, mean_obj + self.cfg.threshold_abs) + lower_cut = min(mean_obj - self.cfg.threshold_std * std_obj, mean_obj - self.cfg.threshold_abs) + leaders = [p for obj, p in zip(initialized_objectives, initialized_policies) if obj > upper_cut] + underperformers = [p for obj, p in zip(initialized_objectives, initialized_policies) if obj < lower_cut] + + print(f"mean={mean_obj:.4f}, std={std_obj:.4f}, upper={upper_cut:.4f}, lower={lower_cut:.4f}") + print(f"Leaders: {leaders} Underperformers: {underperformers}") + + # 3) Only replace if *this* policy is an underperformer + if self.cfg.policy_idx in underperformers: + # 4) If there are any leaders, pick one at random; else simply mutate with no replacement + replacement_policy_candidate = random.choice(leaders) if leaders else self.cfg.policy_idx + print(f"Replacing policy {self.cfg.policy_idx} with {replacement_policy_candidate}.") + + if self.distributed_args.rank == 0: + for param, value in self.pbt_params.items(): + self.algo.writer.add_scalar(f"pbt/{param}", value, self.algo.frame) + self.algo.writer.add_scalar("pbt/00_best_objective", max(initialized_objectives), self.algo.frame) + self.algo.writer.flush() + + # Decided to replace the policy weights! + cur_params = ckpts[replacement_policy_candidate]["params"] + self.new_params = mutate(cur_params, self.cfg.mutation, self.cfg.mutation_rate, self.cfg.change_range) + self.restart_from_checkpoint = os.path.abspath(ckpts[replacement_policy_candidate]["checkpoint"]) + self.restart_flag[0] = 1 + if self.distributed_args.distributed: + dist.broadcast(self.restart_flag, src=0) + + self.printer.print_mutation_diff(cur_params, self.new_params) + + def _restart_with_new_params(self, new_params, restart_from_checkpoint): + """Re-exec the current process with a filtered/augmented CLI to apply new params. + + Notes: + - Filters out existing Hydra-style overrides that will be replaced, + and appends `--checkpoint=` and new param overrides. + - On distributed runs, assigns a fresh master port and forwards + distributed args to the python.sh launcher. + """ + cli_args = sys.argv + print(f"previous command line args: {cli_args}") + + SKIP = ["checkpoint"] + is_hydra = lambda arg: ( # noqa: E731 + (name := arg.split("=", 1)[0]) not in new_params and not any(k in name for k in SKIP) + ) + modified_args = [cli_args[0]] + [arg for arg in cli_args[1:] if "=" not in arg or is_hydra(arg)] + + modified_args.append(f"--checkpoint={restart_from_checkpoint}") + modified_args.extend(self.wandb_args.get_args_list()) + modified_args.extend(self.rendering_args.get_args_list()) + + # add all of the new (possibly mutated) parameters + for param, value in new_params.items(): + modified_args.append(f"{param}={value}") + + self.algo.writer.flush() + self.algo.writer.close() + + if self.wandb_args.enabled: + import wandb + + wandb.run.finish() + + # Get the directory of the current file + thisfile_dir = os.path.dirname(os.path.abspath(__file__)) + isaac_sim_path = os.path.abspath(os.path.join(thisfile_dir, "../../../../../_isaac_sim")) + command = [f"{isaac_sim_path}/python.sh"] + + if self.distributed_args.distributed: + self.distributed_args.master_port = str(pbt_utils.find_free_port()) + command.extend(self.distributed_args.get_args_list()) + command += [modified_args[0]] + command.extend(self.env_args.get_args_list()) + command += modified_args[1:] + if self.distributed_args.distributed: + command += ["--distributed"] + + print("Running command:", command, flush=True) + print("sys.executable = ", sys.executable) + print(f"Policy {self.cfg.policy_idx}: Restarting self with args {modified_args}", flush=True) + + if self.distributed_args.rank == 0: + pbt_utils.dump_env_sizes() + + # after any sourcing (or before exec’ing python.sh) prevent kept increasing arg_length: + for var in ("PATH", "PYTHONPATH", "LD_LIBRARY_PATH", "OMNI_USD_RESOLVER_MDL_BUILTIN_PATHS"): + val = os.environ.get(var) + if not val or os.pathsep not in val: + continue + seen = set() + new_parts = [] + for p in val.split(os.pathsep): + if p and p not in seen: + seen.add(p) + new_parts.append(p) + os.environ[var] = os.pathsep.join(new_parts) + + os.execv(f"{isaac_sim_path}/python.sh", command) + + +class MultiObserver(AlgoObserver): + """Meta-observer that allows the user to add several observers.""" + + def __init__(self, observers_): + super().__init__() + self.observers = observers_ + + def _call_multi(self, method, *args_, **kwargs_): + for o in self.observers: + getattr(o, method)(*args_, **kwargs_) + + def before_init(self, base_name, config, experiment_name): + self._call_multi("before_init", base_name, config, experiment_name) + + def after_init(self, algo): + self._call_multi("after_init", algo) + + def process_infos(self, infos, done_indices): + self._call_multi("process_infos", infos, done_indices) + + def after_steps(self): + self._call_multi("after_steps") + + def after_clear_stats(self): + self._call_multi("after_clear_stats") + + def after_print_stats(self, frame, epoch_num, total_time): + self._call_multi("after_print_stats", frame, epoch_num, total_time) diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py new file mode 100644 index 00000000000..63cc534edd6 --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_cfg.py @@ -0,0 +1,63 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from isaaclab.utils import configclass + + +@configclass +class PbtCfg: + """ + Population-Based Training (PBT) configuration. + + leaders are policies with score > max(mean + threshold_std*std, mean + threshold_abs). + underperformers are policies with score < min(mean - threshold_std*std, mean - threshold_abs). + On replacement, selected hyperparameters are mutated multiplicatively in [change_min, change_max]. + """ + + enabled: bool = False + """Enable/disable PBT logic.""" + + policy_idx: int = 0 + """Index of this learner in the population (unique in [0, num_policies-1]).""" + + num_policies: int = 8 + """Total number of learners participating in PBT.""" + + directory: str = "" + """Root directory for PBT artifacts (checkpoints, metadata).""" + + workspace: str = "pbt_workspace" + """Subfolder under the training dir to isolate this PBT run.""" + + objective: str = "Episode_Reward/success" + """The key in info returned by env.step that pbt measures to determine leaders and underperformers, + If reward is stationary, using the term that corresponds to task success is usually enough, when reward + are non-stationary, consider uses better objectives. + """ + + interval_steps: int = 100_000 + """Environment steps between PBT iterations (save, compare, replace/mutate).""" + + threshold_std: float = 0.10 + """Std-based margin k in max(mean ± k·std, mean ± threshold_abs) for leader/underperformer cuts.""" + + threshold_abs: float = 0.05 + """Absolute margin A in max(mean ± threshold_std·std, mean ± A) for leader/underperformer cuts.""" + + mutation_rate: float = 0.25 + """Per-parameter probability of mutation when a policy is replaced.""" + + change_range: tuple[float, float] = (1.1, 2.0) + """Lower and upper bound of multiplicative change factor (sampled in [change_min, change_max]).""" + + mutation: dict[str, str] = {} + """Mutation strings indicating which parameter will be mutated when pbt restart + example: + { + "agent.params.config.learning_rate": "mutate_float" + "agent.params.config.grad_norm": "mutate_float" + "agent.params.config.entropy_coef": "mutate_float" + } + """ diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py new file mode 100644 index 00000000000..2ce88010af5 --- /dev/null +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt_utils.py @@ -0,0 +1,295 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import datetime +import os +import random +import socket +import yaml +from collections import OrderedDict +from pathlib import Path +from prettytable import PrettyTable + +from rl_games.algos_torch.torch_ext import safe_filesystem_op, safe_save + + +class DistributedArgs: + def __init__(self, args_cli): + self.distributed = args_cli.distributed + self.nproc_per_node = int(os.environ.get("WORLD_SIZE", 1)) + self.rank = int(os.environ.get("RANK", 0)) + self.nnodes = 1 + self.master_port = getattr(args_cli, "master_port", None) + + def get_args_list(self) -> list[str]: + args = ["-m", "torch.distributed.run", f"--nnodes={self.nnodes}", f"--nproc_per_node={self.nproc_per_node}"] + if self.master_port: + args.append(f"--master_port={self.master_port}") + return args + + +class EnvArgs: + def __init__(self, args_cli): + self.task = args_cli.task + self.seed = args_cli.seed if args_cli.seed is not None else -1 + self.headless = args_cli.headless + self.num_envs = args_cli.num_envs + + def get_args_list(self) -> list[str]: + list = [] + list.append(f"--task={self.task}") + list.append(f"--seed={self.seed}") + list.append(f"--num_envs={self.num_envs}") + if self.headless: + list.append("--headless") + return list + + +class RenderingArgs: + def __init__(self, args_cli): + self.camera_enabled = args_cli.enable_cameras + self.video = args_cli.video + self.video_length = args_cli.video_length + self.video_interval = args_cli.video_interval + + def get_args_list(self) -> list[str]: + args = [] + if self.camera_enabled: + args.append("--enable_cameras") + if self.video: + args.extend(["--video", f"--video_length={self.video_length}", f"--video_interval={self.video_interval}"]) + return args + + +class WandbArgs: + def __init__(self, args_cli): + self.enabled = args_cli.track + self.project_name = args_cli.wandb_project_name + self.name = args_cli.wandb_name + self.entity = args_cli.wandb_entity + + def get_args_list(self) -> list[str]: + args = [] + if self.enabled: + args.append("--track") + if self.entity: + args.append(f"--wandb-entity={self.entity}") + else: + raise ValueError("entity must be specified if wandb is enabled") + if self.project_name: + args.append(f"--wandb-project-name={self.project_name}") + if self.name: + args.append(f"--wandb-name={self.name}") + return args + + +def dump_env_sizes(): + """Print summary of environment variable usage (count, bytes, top-5 largest, SC_ARG_MAX).""" + + n = len(os.environ) + # total bytes in "KEY=VAL\0" for all envp entries + total = sum(len(k) + 1 + len(v) + 1 for k, v in os.environ.items()) + # find the 5 largest values + biggest = sorted(os.environ.items(), key=lambda kv: len(kv[1]), reverse=True)[:5] + + print(f"[ENV MONITOR] vars={n}, total_bytes={total}") + for k, v in biggest: + print(f" {k!r} length={len(v)} → {v[:60]}{'…' if len(v) > 60 else ''}") + + try: + argmax = os.sysconf("SC_ARG_MAX") + print(f"[ENV MONITOR] SC_ARG_MAX = {argmax}") + except (ValueError, AttributeError): + pass + + +def flatten_dict(d, prefix="", separator="."): + """Flatten nested dictionaries into a flat dict with keys joined by `separator`.""" + + res = dict() + for key, value in d.items(): + if isinstance(value, (dict, OrderedDict)): + res.update(flatten_dict(value, prefix + key + separator, separator)) + else: + res[prefix + key] = value + + return res + + +def find_free_port(max_tries: int = 20) -> int: + """Return an OS-assigned free TCP port, with a few retries; fall back to a random high port.""" + for _ in range(max_tries): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind(("", 0)) + return s.getsockname()[1] + except OSError: + continue + return random.randint(20000, 65000) + + +def filter_params(params, params_to_mutate): + """Filter `params` to only those in `params_to_mutate`, converting str floats (e.g. '1e-4') to float.""" + + def try_float(v): + if isinstance(v, str): + try: + return float(v) + except ValueError: + return v + return v + + return {k: try_float(v) for k, v in params.items() if k in params_to_mutate} + + +def save_pbt_checkpoint(workspace_dir, curr_policy_score, curr_iter, algo, params): + """Save a PBT checkpoint (.pth and .yaml) with policy state, score, and metadata (rank 0 only).""" + if int(os.environ.get("RANK", "0")) == 0: + checkpoint_file = os.path.join(workspace_dir, f"{curr_iter:06d}.pth") + safe_save(algo.get_full_state_weights(), checkpoint_file) + pbt_checkpoint_file = os.path.join(workspace_dir, f"{curr_iter:06d}.yaml") + + pbt_checkpoint = { + "iteration": curr_iter, + "true_objective": curr_policy_score, + "frame": algo.frame, + "params": params, + "checkpoint": os.path.abspath(checkpoint_file), + "pbt_checkpoint": os.path.abspath(pbt_checkpoint_file), + "experiment_name": algo.experiment_name, + } + + with open(pbt_checkpoint_file, "w") as fobj: + yaml.dump(pbt_checkpoint, fobj) + + +def load_pbt_ckpts(workspace_dir, cur_policy_id, num_policies, pbt_iteration) -> dict | None: + """ + Load the latest available PBT checkpoint for each policy (≤ current iteration). + Returns a dict mapping policy_idx → checkpoint dict or None. (rank 0 only) + """ + if int(os.environ.get("RANK", "0")) != 0: + return None + checkpoints = dict() + for policy_idx in range(num_policies): + checkpoints[policy_idx] = None + policy_dir = os.path.join(workspace_dir, f"{policy_idx:03d}") + + if not os.path.isdir(policy_dir): + continue + + pbt_checkpoint_files = sorted([f for f in os.listdir(policy_dir) if f.endswith(".yaml")], reverse=True) + for pbt_checkpoint_file in pbt_checkpoint_files: + iteration = int(pbt_checkpoint_file.split(".")[0]) + + # current local time + now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ctime_ts = os.path.getctime(os.path.join(policy_dir, pbt_checkpoint_file)) + created_str = datetime.datetime.fromtimestamp(ctime_ts).strftime("%Y-%m-%d %H:%M:%S") + + if iteration <= pbt_iteration: + with open(os.path.join(policy_dir, pbt_checkpoint_file)) as fobj: + now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print( + f"Policy {cur_policy_id} [{now_str}]: Loading" + f" policy-{policy_idx} {pbt_checkpoint_file} (created at {created_str})" + ) + checkpoints[policy_idx] = safe_filesystem_op(yaml.load, fobj, Loader=yaml.FullLoader) + break + + return checkpoints + + +def cleanup(checkpoints: dict[int, dict], policy_dir, keep_back: int = 20, max_yaml: int = 50) -> None: + """ + Cleanup old checkpoints for the current policy directory (rank 0 only). + - Delete files older than (oldest iteration - keep_back). + - Keep at most `max_yaml` latest YAML iterations. + """ + if int(os.environ.get("RANK", "0")) == 0: + oldest = min((ckpt["iteration"] if ckpt else 0) for ckpt in checkpoints.values()) + threshold = max(0, oldest - keep_back) + root = Path(policy_dir) + + # group files by numeric iteration (only *.yaml / *.pth) + groups: dict[int, list[Path]] = {} + for p in root.iterdir(): + if p.suffix in (".yaml", ".pth") and p.stem.isdigit(): + groups.setdefault(int(p.stem), []).append(p) + + # 1) drop anything older than threshold + for it in [i for i in groups if i <= threshold]: + for p in groups[it]: + p.unlink(missing_ok=True) + groups.pop(it, None) + + # 2) cap total YAML checkpoints: keep newest `max_yaml` iters + yaml_iters = sorted((i for i, ps in groups.items() if any(p.suffix == ".yaml" for p in ps)), reverse=True) + for it in yaml_iters[max_yaml:]: + for p in groups.get(it, []): + p.unlink(missing_ok=True) + groups.pop(it, None) + + +class PbtTablePrinter: + """All PrettyTable-related rendering lives here.""" + + def __init__(self, *, float_digits: int = 6, path_maxlen: int = 52): + self.float_digits = float_digits + self.path_maxlen = path_maxlen + + # format helpers + def fmt(self, v): + return f"{v:.{self.float_digits}g}" if isinstance(v, float) else v + + def short(self, s: str) -> str: + s = str(s) + L = self.path_maxlen + return s if len(s) <= L else s[: L // 2 - 1] + "…" + s[-L // 2 :] + + # tables + def print_params_table(self, params: dict, header: str = "Parameters"): + table = PrettyTable(field_names=["Parameter", "Value"]) + table.align["Parameter"] = "l" + table.align["Value"] = "r" + for k in sorted(params): + table.add_row([k, self.fmt(params[k])]) + print(header + ":") + print(table.get_string()) + + def print_ckpt_summary(self, sumry: dict[int, dict | None]): + t = PrettyTable(["Policy", "Status", "Objective", "Iter", "Frame", "Experiment", "Checkpoint", "YAML"]) + t.align["Policy"] = "r" + t.align["Status"] = "l" + t.align["Objective"] = "r" + t.align["Iter"] = "r" + t.align["Frame"] = "r" + t.align["Experiment"] = "l" + t.align["Checkpoint"] = "l" + t.align["YAML"] = "l" + for p in sorted(sumry.keys()): + c = sumry[p] + if c is None: + t.add_row([p, "—", "", "", "", "", "", ""]) + else: + t.add_row([ + p, + "OK", + self.fmt(c.get("true_objective", "")), + c.get("iteration", ""), + c.get("frame", ""), + c.get("experiment_name", ""), + self.short(c.get("checkpoint", "")), + self.short(c.get("pbt_checkpoint", "")), + ]) + print(t) + + def print_mutation_diff(self, before: dict, after: dict, *, header: str = "Mutated params (changed only)"): + t = PrettyTable(["Parameter", "Old", "New"]) + for k in sorted(before): + if before[k] != after[k]: + t.add_row([k, self.fmt(before[k]), self.fmt(after[k])]) + print(header + ":") + print(t if t._rows else "(no changes)") diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games.py b/source/isaaclab_rl/isaaclab_rl/rl_games/rl_games.py similarity index 100% rename from source/isaaclab_rl/isaaclab_rl/rl_games.py rename to source/isaaclab_rl/isaaclab_rl/rl_games/rl_games.py From 82169500ff65702bba366fc421b64c4f95ff0bf1 Mon Sep 17 00:00:00 2001 From: rebeccazhang0707 <168459200+rebeccazhang0707@users.noreply.github.com> Date: Wed, 10 Sep 2025 07:30:05 +0800 Subject: [PATCH 11/50] Fixes the import issues in stacking manipulation task (#3398) # Description Fixes the import issues via "path:Module" in below files, which will not import all modules/dependencies unless in use ``` - isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py - isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py - isaaclab_mimic/envs/__init__.py ``` ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../isaaclab_mimic/envs/__init__.py | 31 ++++++++++--------- .../stack/config/galbot/__init__.py | 15 +++++---- .../stack/config/ur10_gripper/__init__.py | 6 ++-- .../config/ur10_gripper/agents/__init__.py | 4 --- 4 files changed, 25 insertions(+), 31 deletions(-) delete mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/agents/__init__.py diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py index 5c80d5ddbcd..37bcfea5156 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py @@ -16,13 +16,6 @@ from .franka_stack_ik_rel_skillgen_env_cfg import FrankaCubeStackIKRelSkillgenEnvCfg from .franka_stack_ik_rel_visuomotor_cosmos_mimic_env_cfg import FrankaCubeStackIKRelVisuomotorCosmosMimicEnvCfg from .franka_stack_ik_rel_visuomotor_mimic_env_cfg import FrankaCubeStackIKRelVisuomotorMimicEnvCfg -from .galbot_stack_rmp_abs_mimic_env import RmpFlowGalbotCubeStackAbsMimicEnv -from .galbot_stack_rmp_abs_mimic_env_cfg import ( - RmpFlowGalbotLeftArmGripperCubeStackAbsMimicEnvCfg, - RmpFlowGalbotRightArmSuctionCubeStackAbsMimicEnvCfg, -) -from .galbot_stack_rmp_rel_mimic_env import RmpFlowGalbotCubeStackRelMimicEnv -from .galbot_stack_rmp_rel_mimic_env_cfg import RmpFlowGalbotLeftArmGripperCubeStackRelMimicEnvCfg ## # Inverse Kinematics - Relative Pose Control @@ -104,18 +97,22 @@ gym.register( id="Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-Rel-Mimic-v0", - entry_point="isaaclab_mimic.envs:RmpFlowGalbotCubeStackRelMimicEnv", + entry_point=f"{__name__}.galbot_stack_rmp_rel_mimic_env:RmpFlowGalbotCubeStackRelMimicEnv", kwargs={ - "env_cfg_entry_point": galbot_stack_rmp_rel_mimic_env_cfg.RmpFlowGalbotLeftArmGripperCubeStackRelMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.galbot_stack_rmp_rel_mimic_env_cfg:RmpFlowGalbotLeftArmGripperCubeStackRelMimicEnvCfg" + ), }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-Rel-Mimic-v0", - entry_point="isaaclab_mimic.envs:RmpFlowGalbotCubeStackRelMimicEnv", + entry_point=f"{__name__}.galbot_stack_rmp_rel_mimic_env:RmpFlowGalbotCubeStackRelMimicEnv", kwargs={ - "env_cfg_entry_point": galbot_stack_rmp_rel_mimic_env_cfg.RmpFlowGalbotRightArmSuctionCubeStackRelMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.galbot_stack_rmp_rel_mimic_env_cfg:RmpFlowGalbotRightArmSuctionCubeStackRelMimicEnvCfg" + ), }, disable_env_checker=True, ) @@ -126,18 +123,22 @@ gym.register( id="Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs:RmpFlowGalbotCubeStackAbsMimicEnv", + entry_point=f"{__name__}.galbot_stack_rmp_abs_mimic_env:RmpFlowGalbotCubeStackAbsMimicEnv", kwargs={ - "env_cfg_entry_point": galbot_stack_rmp_abs_mimic_env_cfg.RmpFlowGalbotLeftArmGripperCubeStackAbsMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.galbot_stack_rmp_abs_mimic_env_cfg:RmpFlowGalbotLeftArmGripperCubeStackAbsMimicEnvCfg" + ), }, disable_env_checker=True, ) gym.register( id="Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-Abs-Mimic-v0", - entry_point="isaaclab_mimic.envs:RmpFlowGalbotCubeStackAbsMimicEnv", + entry_point=f"{__name__}.galbot_stack_rmp_abs_mimic_env:RmpFlowGalbotCubeStackAbsMimicEnv", kwargs={ - "env_cfg_entry_point": galbot_stack_rmp_abs_mimic_env_cfg.RmpFlowGalbotRightArmSuctionCubeStackAbsMimicEnvCfg, + "env_cfg_entry_point": ( + f"{__name__}.galbot_stack_rmp_abs_mimic_env_cfg:RmpFlowGalbotRightArmSuctionCubeStackAbsMimicEnvCfg" + ), }, disable_env_checker=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py index 06bc8dcbf06..7aa2ebad0fc 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/galbot/__init__.py @@ -5,9 +5,6 @@ import gymnasium as gym -import os - -from . import stack_rmp_rel_env_cfg ## # Register Gym environments. @@ -21,7 +18,7 @@ id="Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_rmp_rel_env_cfg.RmpFlowGalbotLeftArmCubeStackEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_rmp_rel_env_cfg:RmpFlowGalbotLeftArmCubeStackEnvCfg", }, disable_env_checker=True, ) @@ -31,7 +28,7 @@ id="Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_rmp_rel_env_cfg.RmpFlowGalbotRightArmCubeStackEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_rmp_rel_env_cfg:RmpFlowGalbotRightArmCubeStackEnvCfg", }, disable_env_checker=True, ) @@ -44,7 +41,7 @@ id="Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_rmp_rel_env_cfg.RmpFlowGalbotLeftArmCubeStackVisuomotorEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_rmp_rel_env_cfg:RmpFlowGalbotLeftArmCubeStackVisuomotorEnvCfg", }, disable_env_checker=True, ) @@ -56,7 +53,9 @@ id="Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Joint-Position-Play-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_rmp_rel_env_cfg.GalbotLeftArmJointPositionCubeStackVisuomotorEnvCfg_PLAY, + "env_cfg_entry_point": ( + f"{__name__}.stack_rmp_rel_env_cfg:GalbotLeftArmJointPositionCubeStackVisuomotorEnvCfg_PLAY" + ), }, disable_env_checker=True, ) @@ -68,7 +67,7 @@ id="Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-RmpFlow-Play-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_rmp_rel_env_cfg.GalbotLeftArmRmpFlowCubeStackVisuomotorEnvCfg_PLAY, + "env_cfg_entry_point": f"{__name__}.stack_rmp_rel_env_cfg:GalbotLeftArmRmpFlowCubeStackVisuomotorEnvCfg_PLAY", }, disable_env_checker=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py index d051b5fc548..41887a8df8b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/__init__.py @@ -5,8 +5,6 @@ import gymnasium as gym -from . import stack_ik_rel_env_cfg - ## # Register Gym environments. ## @@ -20,7 +18,7 @@ id="Isaac-Stack-Cube-UR10-Long-Suction-IK-Rel-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_env_cfg.UR10LongSuctionCubeStackEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_ik_rel_env_cfg:UR10LongSuctionCubeStackEnvCfg", }, disable_env_checker=True, ) @@ -29,7 +27,7 @@ id="Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0", entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": stack_ik_rel_env_cfg.UR10ShortSuctionCubeStackEnvCfg, + "env_cfg_entry_point": f"{__name__}.stack_ik_rel_env_cfg:UR10ShortSuctionCubeStackEnvCfg", }, disable_env_checker=True, ) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/agents/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/agents/__init__.py deleted file mode 100644 index 2e924fbf1b1..00000000000 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/ur10_gripper/agents/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause From ca2fd911cdc7f36dd01adfd439a99f37d43b8796 Mon Sep 17 00:00:00 2001 From: rebeccazhang0707 <168459200+rebeccazhang0707@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:08:51 +0800 Subject: [PATCH 12/50] Adds Agibot Humanoid two place tasks (#3228) # Description Adds two place tasks and mimic tasks with Agibot A2D humanoid, using RMPFlow controller: - add agibot robot config in agibot.py and .usd asset - add motion_policy_configs and .urdf for rmpflow controller - add new task cfg: place_toy2box_rmp_rel_env_cfg, and place_upright_mug_rmp_rel_env_cfg - add new mimic task cfg: agibot_place_toy2box_mimic_env_cfg, and agibot_place_upright_mug_mimic_env_cfg - add new mimic task: in pick_place_mimic_env.py - add new subtasks in mdp.observations/terminations: object_grasped, object_placed_upright, object_a_is_into_b Notes: This PR relies on PR (https://github.com/isaac-sim/IsaacLab/pull/3210) for RmpFlowAction support. You can test the whole gr00t-mimic workflow by: 1. Record Demos ``` ./isaaclab.sh -p scripts/tools/record_demos.py \ --dataset_file datasets/recorded_demos_agibot_right_arm_rel.hdf5 \ --task Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 \ --teleop_device spacemouse \ --num_demos 1 ``` 2. Replay Demos ``` ./isaaclab.sh -p scripts/tools/replay_demos.py \ --dataset_file datasets/recorded_demos_agibot_right_arm_rel.hdf5 \ --task Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 \ --num_envs 1 ``` 3. Annotate Demos ``` ./isaaclab.sh -p scripts/imitation_learning/isaaclab_mimic/annotate_demos.py \ --input_file datasets/recorded_demos_agibot_right_arm_rel.hdf5 \ --output_file datasets/annotated_demos_agibot_right_arm_rel.hdf5 \ --task Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-Rel-Mimic-v0 \ --auto ``` 4. Generate Demos ``` ./isaaclab.sh -p scripts/imitation_learning/isaaclab_mimic/generate_dataset.py \ --input_file datasets/annotated_demos_agibot_right_arm_rel.hdf5 \ --output_file datasets/generated_demos_agibot_right_arm_rel.hdf5 \ --task Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-Rel-Mimic-v0 \ --num_envs 16 \ --generation_num_trials 10 ``` ## Type of change - New feature (non-breaking change which adds functionality) ## Screenshot environments_agibot ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: rebeccazhang0707 <168459200+rebeccazhang0707@users.noreply.github.com> Signed-off-by: Kelly Guo Co-authored-by: Kelly Guo --- .../tasks/manipulation/agibot_place_mug.jpg | Bin 0 -> 77293 bytes .../tasks/manipulation/agibot_place_toy.jpg | Bin 0 -> 81016 bytes docs/source/overview/environments.rst | 38 ++ .../isaaclab/controllers/config/rmp_flow.py | 20 + .../isaaclab_assets/robots/agibot.py | 160 ++++++++ .../isaaclab_mimic/envs/__init__.py | 25 ++ .../agibot_place_toy2box_mimic_env_cfg.py | 84 +++++ .../agibot_place_upright_mug_mimic_env_cfg.py | 81 ++++ .../envs/pick_place_mimic_env.py | 178 +++++++++ .../manipulation/place/__init__.py | 9 + .../manipulation/place/config/__init__.py | 9 + .../place/config/agibot/__init__.py | 34 ++ .../agibot/place_toy2box_rmp_rel_env_cfg.py | 356 ++++++++++++++++++ .../place_upright_mug_rmp_rel_env_cfg.py | 284 ++++++++++++++ .../manipulation/place/mdp/__init__.py | 11 + .../manipulation/place/mdp/observations.py | 118 ++++++ .../manipulation/place/mdp/terminations.py | 122 ++++++ 17 files changed, 1529 insertions(+) create mode 100644 docs/source/_static/tasks/manipulation/agibot_place_mug.jpg create mode 100644 docs/source/_static/tasks/manipulation/agibot_place_toy.jpg create mode 100644 source/isaaclab_assets/isaaclab_assets/robots/agibot.py create mode 100644 source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py create mode 100644 source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py create mode 100644 source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py create mode 100644 source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py diff --git a/docs/source/_static/tasks/manipulation/agibot_place_mug.jpg b/docs/source/_static/tasks/manipulation/agibot_place_mug.jpg new file mode 100644 index 0000000000000000000000000000000000000000..73a421714c232dd175493ab664f0889fd2f5e77f GIT binary patch literal 77293 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τ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 .. |cube-shadow-vis-link| replace:: `Isaac-Repose-Cube-Shadow-Vision-Direct-v0 `__ +.. |agibot_place_mug-link| replace:: `Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 `__ +.. |agibot_place_toy-link| replace:: `Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 `__ + Contact-rich Manipulation ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -987,6 +996,35 @@ inferencing, including reading from an already trained checkpoint and disabling - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Play-v0 - Manager Based - + * - Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Right-Arm-Suction-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-v0 + - Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-Visuomotor-Play-v0 + - Manager Based + - + * - Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 + - + - Manager Based + - + * - Isaac-Velocity-Flat-Anymal-B-v0 - Isaac-Velocity-Flat-Anymal-B-Play-v0 - Manager Based diff --git a/source/isaaclab/isaaclab/controllers/config/rmp_flow.py b/source/isaaclab/isaaclab/controllers/config/rmp_flow.py index e1b18350e14..f3d214168fb 100644 --- a/source/isaaclab/isaaclab/controllers/config/rmp_flow.py +++ b/source/isaaclab/isaaclab/controllers/config/rmp_flow.py @@ -72,3 +72,23 @@ ) """Configuration of RMPFlow for Galbot humanoid.""" + +AGIBOT_LEFT_ARM_RMPFLOW_CFG = RmpFlowControllerCfg( + config_file=os.path.join(ISAACLAB_NUCLEUS_RMPFLOW_DIR, "agibot", "rmpflow", "agibot_left_arm_rmpflow_config.yaml"), + urdf_file=os.path.join(ISAACLAB_NUCLEUS_RMPFLOW_DIR, "agibot", "agibot.urdf"), + collision_file=os.path.join(ISAACLAB_NUCLEUS_RMPFLOW_DIR, "agibot", "rmpflow", "agibot_left_arm_gripper.yaml"), + frame_name="gripper_center", + evaluations_per_frame=5, + ignore_robot_state_updates=True, +) + +AGIBOT_RIGHT_ARM_RMPFLOW_CFG = RmpFlowControllerCfg( + config_file=os.path.join(ISAACLAB_NUCLEUS_RMPFLOW_DIR, "agibot", "rmpflow", "agibot_right_arm_rmpflow_config.yaml"), + urdf_file=os.path.join(ISAACLAB_NUCLEUS_RMPFLOW_DIR, "agibot", "agibot.urdf"), + collision_file=os.path.join(ISAACLAB_NUCLEUS_RMPFLOW_DIR, "agibot", "rmpflow", "agibot_right_arm_gripper.yaml"), + frame_name="right_gripper_center", + evaluations_per_frame=5, + ignore_robot_state_updates=True, +) + +"""Configuration of RMPFlow for Agibot humanoid.""" diff --git a/source/isaaclab_assets/isaaclab_assets/robots/agibot.py b/source/isaaclab_assets/isaaclab_assets/robots/agibot.py new file mode 100644 index 00000000000..4acce179687 --- /dev/null +++ b/source/isaaclab_assets/isaaclab_assets/robots/agibot.py @@ -0,0 +1,160 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configuration for the Agibot A2D humanoid robots. + +The following configurations are available: + +* :obj:`AGIBOT_A2D_CFG`: Agibot A2D robot + + +""" + +import isaaclab.sim as sim_utils +from isaaclab.actuators import ImplicitActuatorCfg +from isaaclab.assets.articulation import ArticulationCfg +from isaaclab.utils.assets import ISAACLAB_NUCLEUS_DIR + +## +# Configuration +## + +AGIBOT_A2D_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/Agibot/A2D/A2D_physics.usd", + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=8, + solver_velocity_iteration_count=0, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + # Body joints + "joint_lift_body": 0.1995, + "joint_body_pitch": 0.6025, + # Head joints + "joint_head_yaw": 0.0, + "joint_head_pitch": 0.6708, + # Left arm joints + "left_arm_joint1": -1.0817, + "left_arm_joint2": 0.5907, + "left_arm_joint3": 0.3442, + "left_arm_joint4": -1.2819, + "left_arm_joint5": 0.6928, + "left_arm_joint6": 1.4725, + "left_arm_joint7": -0.1599, + # Right arm joints + "right_arm_joint1": 1.0817, + "right_arm_joint2": -0.5907, + "right_arm_joint3": -0.3442, + "right_arm_joint4": 1.2819, + "right_arm_joint5": -0.6928, + "right_arm_joint6": -0.7, + "right_arm_joint7": 0.0, + # Left gripper joints + "left_Right_1_Joint": 0.0, + "left_hand_joint1": 0.994, + "left_Right_0_Joint": 0.0, + "left_Left_0_Joint": 0.0, + "left_Right_Support_Joint": 0.994, + "left_Left_Support_Joint": 0.994, + "left_Right_RevoluteJoint": 0.0, + "left_Left_RevoluteJoint": 0.0, + # Right gripper joints + "right_Right_1_Joint": 0.0, + "right_hand_joint1": 0.994, + "right_Right_0_Joint": 0.0, + "right_Left_0_Joint": 0.0, + "right_Right_Support_Joint": 0.994, + "right_Left_Support_Joint": 0.994, + "right_Right_RevoluteJoint": 0.0, + "right_Left_RevoluteJoint": 0.0, + }, + pos=(-0.6, 0.0, -1.05), # init pos of the articulation for teleop + ), + actuators={ + # Body lift and torso actuators + "body": ImplicitActuatorCfg( + joint_names_expr=["joint_lift_body", "joint_body_pitch"], + effort_limit_sim=10000.0, + velocity_limit_sim=2.61, + stiffness=10000000.0, + damping=200.0, + ), + # Head actuators + "head": ImplicitActuatorCfg( + joint_names_expr=["joint_head_yaw", "joint_head_pitch"], + effort_limit_sim=50.0, + velocity_limit_sim=1.0, + stiffness=80.0, + damping=4.0, + ), + # Left arm actuator + "left_arm": ImplicitActuatorCfg( + joint_names_expr=["left_arm_joint[1-7]"], + effort_limit_sim={ + "left_arm_joint1": 2000.0, + "left_arm_joint[2-7]": 1000.0, + }, + velocity_limit_sim=1.57, + stiffness={"left_arm_joint1": 10000000.0, "left_arm_joint[2-7]": 20000.0}, + damping={"left_arm_joint1": 0.0, "left_arm_joint[2-7]": 0.0}, + ), + # Right arm actuator + "right_arm": ImplicitActuatorCfg( + joint_names_expr=["right_arm_joint[1-7]"], + effort_limit_sim={ + "right_arm_joint1": 2000.0, + "right_arm_joint[2-7]": 1000.0, + }, + velocity_limit_sim=1.57, + stiffness={"right_arm_joint1": 10000000.0, "right_arm_joint[2-7]": 20000.0}, + damping={"right_arm_joint1": 0.0, "right_arm_joint[2-7]": 0.0}, + ), + # "left_Right_2_Joint" is excluded from Articulation. + # "left_hand_joint1" is the driver joint, and "left_Right_1_Joint" is the mimic joint. + # "left_.*_Support_Joint" driver joint can be set optionally, to disable the driver, set stiffness and damping to 0.0 below + "left_gripper": ImplicitActuatorCfg( + joint_names_expr=["left_hand_joint1", "left_.*_Support_Joint"], + effort_limit_sim={"left_hand_joint1": 10.0, "left_.*_Support_Joint": 1.0}, + velocity_limit_sim=2.0, + stiffness={"left_hand_joint1": 20.0, "left_.*_Support_Joint": 2.0}, + damping={"left_hand_joint1": 0.10, "left_.*_Support_Joint": 0.01}, + ), + # set PD to zero for passive joints in close-loop gripper + "left_gripper_passive": ImplicitActuatorCfg( + joint_names_expr=["left_.*_(0|1)_Joint", "left_.*_RevoluteJoint"], + effort_limit_sim=10.0, + velocity_limit_sim=10.0, + stiffness=0.0, + damping=0.0, + ), + # "right_Right_2_Joint" is excluded from Articulation. + # "right_hand_joint1" is the driver joint, and "right_Right_1_Joint" is the mimic joint. + # "right_.*_Support_Joint" driver joint can be set optionally, to disable the driver, set stiffness and damping to 0.0 below + "right_gripper": ImplicitActuatorCfg( + joint_names_expr=["right_hand_joint1", "right_.*_Support_Joint"], + effort_limit_sim={"right_hand_joint1": 100.0, "right_.*_Support_Joint": 100.0}, + velocity_limit_sim=10.0, + stiffness={"right_hand_joint1": 20.0, "right_.*_Support_Joint": 2.0}, + damping={"right_hand_joint1": 0.10, "right_.*_Support_Joint": 0.01}, + ), + # set PD to zero for passive joints in close-loop gripper + "right_gripper_passive": ImplicitActuatorCfg( + joint_names_expr=["right_.*_(0|1)_Joint", "right_.*_RevoluteJoint"], + effort_limit_sim=100.0, + velocity_limit_sim=10.0, + stiffness=0.0, + damping=0.0, + ), + }, + soft_joint_pos_limit_factor=1.0, +) diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py b/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py index 37bcfea5156..bc573b58d51 100644 --- a/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/__init__.py @@ -142,3 +142,28 @@ }, disable_env_checker=True, ) + +## +# Agibot Left Arm: Place Upright Mug with RmpFlow - Relative Pose Control +## +gym.register( + id="Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-Rel-Mimic-v0", + entry_point=f"{__name__}.pick_place_mimic_env:PickPlaceRelMimicEnv", + kwargs={ + "env_cfg_entry_point": ( + f"{__name__}.agibot_place_upright_mug_mimic_env_cfg:RmpFlowAgibotPlaceUprightMugMimicEnvCfg" + ), + }, + disable_env_checker=True, +) +## +# Agibot Right Arm: Place Toy2Box: RmpFlow - Relative Pose Control +## +gym.register( + id="Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-Rel-Mimic-v0", + entry_point=f"{__name__}.pick_place_mimic_env:PickPlaceRelMimicEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.agibot_place_toy2box_mimic_env_cfg:RmpFlowAgibotPlaceToy2BoxMimicEnvCfg", + }, + disable_env_checker=True, +) diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py new file mode 100644 index 00000000000..45e53110ab4 --- /dev/null +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_toy2box_mimic_env_cfg.py @@ -0,0 +1,84 @@ +# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +from isaaclab.envs.mimic_env_cfg import MimicEnvCfg, SubTaskConfig +from isaaclab.utils import configclass + +from isaaclab_tasks.manager_based.manipulation.place.config.agibot.place_toy2box_rmp_rel_env_cfg import ( + RmpFlowAgibotPlaceToy2BoxEnvCfg, +) + +OBJECT_A_NAME = "toy_truck" +OBJECT_B_NAME = "box" + + +@configclass +class RmpFlowAgibotPlaceToy2BoxMimicEnvCfg(RmpFlowAgibotPlaceToy2BoxEnvCfg, MimicEnvCfg): + """ + Isaac Lab Mimic environment config class for Agibot Place Toy2Box env. + """ + + def __post_init__(self): + # post init of parents + super().__post_init__() + + self.datagen_config.name = "demo_src_place_toy2box_task_D0" + self.datagen_config.generation_guarantee = True + self.datagen_config.generation_keep_failed = True + self.datagen_config.generation_num_trials = 10 + self.datagen_config.generation_select_src_per_subtask = True + self.datagen_config.generation_transform_first_robot_pose = False + self.datagen_config.generation_interpolate_from_last_target_pose = True + self.datagen_config.max_num_failures = 25 + self.datagen_config.seed = 1 + + # The following are the subtask configurations for the stack task. + subtask_configs = [] + subtask_configs.append( + SubTaskConfig( + # Each subtask involves manipulation with respect to a single object frame. + object_ref=OBJECT_A_NAME, + # End of final subtask does not need to be detected + subtask_term_signal="grasp", + # No time offsets for the final subtask + subtask_term_offset_range=(2, 10), + # Selection strategy for source subtask segment + selection_strategy="nearest_neighbor_object", + # Optional parameters for the selection strategy function + # selection_strategy_kwargs={"nn_k": 3}, + # Amount of action noise to apply during this subtask + action_noise=0.01, + # Number of interpolation steps to bridge to this subtask segment + num_interpolation_steps=15, + # Additional fixed steps for the robot to reach the necessary pose + num_fixed_steps=0, + # If True, apply action noise during the interpolation phase and execution + apply_noise_during_interpolation=False, + ) + ) + subtask_configs.append( + SubTaskConfig( + # Each subtask involves manipulation with respect to a single object frame. + object_ref=OBJECT_B_NAME, + # End of final subtask does not need to be detected + subtask_term_signal=None, + # No time offsets for the final subtask + subtask_term_offset_range=(0, 0), + # Selection strategy for source subtask segment + selection_strategy="nearest_neighbor_object", + # Optional parameters for the selection strategy function + # selection_strategy_kwargs={"nn_k": 3}, + # Amount of action noise to apply during this subtask + action_noise=0.01, + # Number of interpolation steps to bridge to this subtask segment + num_interpolation_steps=15, + # Additional fixed steps for the robot to reach the necessary pose + num_fixed_steps=0, + # If True, apply action noise during the interpolation phase and execution + apply_noise_during_interpolation=False, + ) + ) + self.subtask_configs["agibot"] = subtask_configs diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py new file mode 100644 index 00000000000..f3154c8f64f --- /dev/null +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/agibot_place_upright_mug_mimic_env_cfg.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +from isaaclab.envs.mimic_env_cfg import MimicEnvCfg, SubTaskConfig +from isaaclab.utils import configclass + +from isaaclab_tasks.manager_based.manipulation.place.config.agibot.place_upright_mug_rmp_rel_env_cfg import ( + RmpFlowAgibotPlaceUprightMugEnvCfg, +) + + +@configclass +class RmpFlowAgibotPlaceUprightMugMimicEnvCfg(RmpFlowAgibotPlaceUprightMugEnvCfg, MimicEnvCfg): + """ + Isaac Lab Mimic environment config class for Agibot Place Upright Mug env. + """ + + def __post_init__(self): + # post init of parents + super().__post_init__() + + self.datagen_config.name = "demo_src_place_upright_mug_task_D0" + self.datagen_config.generation_guarantee = True + self.datagen_config.generation_keep_failed = True + self.datagen_config.generation_num_trials = 10 + self.datagen_config.generation_select_src_per_subtask = True + self.datagen_config.generation_transform_first_robot_pose = False + self.datagen_config.generation_interpolate_from_last_target_pose = True + self.datagen_config.max_num_failures = 25 + self.datagen_config.seed = 1 + + # The following are the subtask configurations for the stack task. + subtask_configs = [] + subtask_configs.append( + SubTaskConfig( + # Each subtask involves manipulation with respect to a single object frame. + object_ref="mug", + # End of final subtask does not need to be detected + subtask_term_signal="grasp", + # No time offsets for the final subtask + subtask_term_offset_range=(15, 30), + # Selection strategy for source subtask segment + selection_strategy="nearest_neighbor_object", + # Optional parameters for the selection strategy function + # selection_strategy_kwargs={"nn_k": 3}, + # Amount of action noise to apply during this subtask + action_noise=0.01, + # Number of interpolation steps to bridge to this subtask segment + num_interpolation_steps=5, + # Additional fixed steps for the robot to reach the necessary pose + num_fixed_steps=0, + # If True, apply action noise during the interpolation phase and execution + apply_noise_during_interpolation=False, + ) + ) + subtask_configs.append( + SubTaskConfig( + # Each subtask involves manipulation with respect to a single object frame. + object_ref="mug", + # End of final subtask does not need to be detected + subtask_term_signal=None, + # No time offsets for the final subtask + subtask_term_offset_range=(0, 0), + # Selection strategy for source subtask segment + selection_strategy="nearest_neighbor_object", + # Optional parameters for the selection strategy function + # selection_strategy_kwargs={"nn_k": 3}, + # Amount of action noise to apply during this subtask + action_noise=0.01, + # Number of interpolation steps to bridge to this subtask segment + num_interpolation_steps=15, + # Additional fixed steps for the robot to reach the necessary pose + num_fixed_steps=0, + # If True, apply action noise during the interpolation phase and execution + apply_noise_during_interpolation=False, + ) + ) + self.subtask_configs["agibot"] = subtask_configs diff --git a/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py b/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py new file mode 100644 index 00000000000..9951c39cf2a --- /dev/null +++ b/source/isaaclab_mimic/isaaclab_mimic/envs/pick_place_mimic_env.py @@ -0,0 +1,178 @@ +# Copyright (c) 2024-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import torch +from collections.abc import Sequence + +import isaaclab.utils.math as PoseUtils + +from .franka_stack_ik_abs_mimic_env import FrankaCubeStackIKAbsMimicEnv +from .franka_stack_ik_rel_mimic_env import FrankaCubeStackIKRelMimicEnv + + +class PickPlaceRelMimicEnv(FrankaCubeStackIKRelMimicEnv): + """ + Isaac Lab Mimic environment wrapper class for DiffIK / RmpFlow Relative Pose Control env. + + This MimicEnv is used when all observations are in the robot base frame. + """ + + def get_object_poses(self, env_ids: Sequence[int] | None = None): + """ + Gets the pose of each object (including rigid objects and articulated objects) in the robot base frame. + + Args: + env_ids: Environment indices to get the pose for. If None, all envs are considered. + + Returns: + A dictionary that maps object names to object pose matrix in robot base frame (4x4 torch.Tensor) + """ + if env_ids is None: + env_ids = slice(None) + + # Get scene state + scene_state = self.scene.get_state(is_relative=True) + rigid_object_states = scene_state["rigid_object"] + articulation_states = scene_state["articulation"] + + # Get robot root pose + robot_root_pose = articulation_states["robot"]["root_pose"] + root_pos = robot_root_pose[env_ids, :3] + root_quat = robot_root_pose[env_ids, 3:7] + + object_pose_matrix = dict() + + # Process rigid objects + for obj_name, obj_state in rigid_object_states.items(): + pos_obj_base, quat_obj_base = PoseUtils.subtract_frame_transforms( + root_pos, root_quat, obj_state["root_pose"][env_ids, :3], obj_state["root_pose"][env_ids, 3:7] + ) + rot_obj_base = PoseUtils.matrix_from_quat(quat_obj_base) + object_pose_matrix[obj_name] = PoseUtils.make_pose(pos_obj_base, rot_obj_base) + + # Process articulated objects (except robot) + for art_name, art_state in articulation_states.items(): + if art_name != "robot": # Skip robot + pos_obj_base, quat_obj_base = PoseUtils.subtract_frame_transforms( + root_pos, root_quat, art_state["root_pose"][env_ids, :3], art_state["root_pose"][env_ids, 3:7] + ) + rot_obj_base = PoseUtils.matrix_from_quat(quat_obj_base) + object_pose_matrix[art_name] = PoseUtils.make_pose(pos_obj_base, rot_obj_base) + + return object_pose_matrix + + def get_subtask_term_signals(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """ + Gets a dictionary of termination signal flags for each subtask in a task. The flag is 1 + when the subtask has been completed and 0 otherwise. The implementation of this method is + required if intending to enable automatic subtask term signal annotation when running the + dataset annotation tool. This method can be kept unimplemented if intending to use manual + subtask term signal annotation. + + Args: + env_ids: Environment indices to get the termination signals for. If None, all envs are considered. + + Returns: + A dictionary termination signal flags (False or True) for each subtask. + """ + if env_ids is None: + env_ids = slice(None) + + signals = dict() + + subtask_terms = self.obs_buf["subtask_terms"] + if "grasp" in subtask_terms: + signals["grasp"] = subtask_terms["grasp"][env_ids] + + # Handle multiple grasp signals + for i in range(0, len(self.cfg.subtask_configs)): + grasp_key = f"grasp_{i + 1}" + if grasp_key in subtask_terms: + signals[grasp_key] = subtask_terms[grasp_key][env_ids] + # final subtask signal is not needed + return signals + + +class PickPlaceAbsMimicEnv(FrankaCubeStackIKAbsMimicEnv): + """ + Isaac Lab Mimic environment wrapper class for DiffIK / RmpFlow Absolute Pose Control env. + + This MimicEnv is used when all observations are in the robot base frame. + """ + + def get_object_poses(self, env_ids: Sequence[int] | None = None): + """ + Gets the pose of each object (including rigid objects and articulated objects) in the robot base frame. + + Args: + env_ids: Environment indices to get the pose for. If None, all envs are considered. + + Returns: + A dictionary that maps object names to object pose matrix in robot base frame (4x4 torch.Tensor) + """ + if env_ids is None: + env_ids = slice(None) + + # Get scene state + scene_state = self.scene.get_state(is_relative=True) + rigid_object_states = scene_state["rigid_object"] + articulation_states = scene_state["articulation"] + + # Get robot root pose + robot_root_pose = articulation_states["robot"]["root_pose"] + root_pos = robot_root_pose[env_ids, :3] + root_quat = robot_root_pose[env_ids, 3:7] + + object_pose_matrix = dict() + + # Process rigid objects + for obj_name, obj_state in rigid_object_states.items(): + pos_obj_base, quat_obj_base = PoseUtils.subtract_frame_transforms( + root_pos, root_quat, obj_state["root_pose"][env_ids, :3], obj_state["root_pose"][env_ids, 3:7] + ) + rot_obj_base = PoseUtils.matrix_from_quat(quat_obj_base) + object_pose_matrix[obj_name] = PoseUtils.make_pose(pos_obj_base, rot_obj_base) + + # Process articulated objects (except robot) + for art_name, art_state in articulation_states.items(): + if art_name != "robot": # Skip robot + pos_obj_base, quat_obj_base = PoseUtils.subtract_frame_transforms( + root_pos, root_quat, art_state["root_pose"][env_ids, :3], art_state["root_pose"][env_ids, 3:7] + ) + rot_obj_base = PoseUtils.matrix_from_quat(quat_obj_base) + object_pose_matrix[art_name] = PoseUtils.make_pose(pos_obj_base, rot_obj_base) + + return object_pose_matrix + + def get_subtask_term_signals(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """ + Gets a dictionary of termination signal flags for each subtask in a task. The flag is 1 + when the subtask has been completed and 0 otherwise. The implementation of this method is + required if intending to enable automatic subtask term signal annotation when running the + dataset annotation tool. This method can be kept unimplemented if intending to use manual + subtask term signal annotation. + + Args: + env_ids: Environment indices to get the termination signals for. If None, all envs are considered. + + Returns: + A dictionary termination signal flags (False or True) for each subtask. + """ + if env_ids is None: + env_ids = slice(None) + + signals = dict() + + subtask_terms = self.obs_buf["subtask_terms"] + if "grasp" in subtask_terms: + signals["grasp"] = subtask_terms["grasp"][env_ids] + + # Handle multiple grasp signals + for i in range(0, len(self.cfg.subtask_configs)): + grasp_key = f"grasp_{i + 1}" + if grasp_key in subtask_terms: + signals[grasp_key] = subtask_terms[grasp_key][env_ids] + # final subtask signal is not needed + return signals diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py new file mode 100644 index 00000000000..d2bbb58b0cb --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for the place environments.""" + +# We leave this file empty since we don't want to expose any configs in this package directly. +# We still need this file to import the "config" module in the parent package. diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py new file mode 100644 index 00000000000..d2bbb58b0cb --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Configurations for the place environments.""" + +# We leave this file empty since we don't want to expose any configs in this package directly. +# We still need this file to import the "config" module in the parent package. diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py new file mode 100644 index 00000000000..6941186bea4 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/__init__.py @@ -0,0 +1,34 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import gymnasium as gym + +## +# Register Gym environments. +## + +## +# Agibot Right Arm: place toy2box task, with RmpFlow +## +gym.register( + id="Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.place_toy2box_rmp_rel_env_cfg:RmpFlowAgibotPlaceToy2BoxEnvCfg", + }, + disable_env_checker=True, +) + +## +# Agibot Left Arm: place upright mug task, with RmpFlow +## +gym.register( + id="Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0", + entry_point="isaaclab.envs:ManagerBasedRLEnv", + kwargs={ + "env_cfg_entry_point": f"{__name__}.place_upright_mug_rmp_rel_env_cfg:RmpFlowAgibotPlaceUprightMugEnvCfg", + }, + disable_env_checker=True, +) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py new file mode 100644 index 00000000000..63f9f193186 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py @@ -0,0 +1,356 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +from dataclasses import MISSING + +from isaaclab_assets import ISAACLAB_ASSETS_DATA_DIR + +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.devices.device_base import DevicesCfg +from isaaclab.devices.keyboard import Se3KeyboardCfg +from isaaclab.devices.spacemouse import Se3SpaceMouseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.envs.mdp.actions.rmpflow_actions_cfg import RMPFlowActionCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.sensors import ContactSensorCfg, FrameTransformerCfg +from isaaclab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from isaaclab.sim.schemas.schemas_cfg import MassPropertiesCfg, RigidBodyPropertiesCfg +from isaaclab.sim.spawners.from_files.from_files_cfg import UsdFileCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from isaaclab_tasks.manager_based.manipulation.place import mdp as place_mdp +from isaaclab_tasks.manager_based.manipulation.stack import mdp +from isaaclab_tasks.manager_based.manipulation.stack.mdp import franka_stack_events +from isaaclab_tasks.manager_based.manipulation.stack.stack_env_cfg import ObjectTableSceneCfg + +## +# Pre-defined configs +## +from isaaclab.markers.config import FRAME_MARKER_CFG # isort: skip +from isaaclab_assets.robots.agibot import AGIBOT_A2D_CFG # isort: skip +from isaaclab.controllers.config.rmp_flow import AGIBOT_RIGHT_ARM_RMPFLOW_CFG # isort: skip + +## +# Event settings +## + + +@configclass +class EventCfgPlaceToy2Box: + """Configuration for events.""" + + reset_all = EventTerm(func=mdp.reset_scene_to_default, mode="reset", params={"reset_joint_targets": True}) + + init_toy_position = EventTerm( + func=franka_stack_events.randomize_object_pose, + mode="reset", + params={ + "pose_range": { + "x": (-0.15, 0.20), + "y": (-0.3, -0.15), + "z": (-0.65, -0.65), + "roll": (1.57, 1.57), + "yaw": (-3.14, 3.14), + }, + "asset_cfgs": [SceneEntityCfg("toy_truck")], + }, + ) + init_box_position = EventTerm( + func=franka_stack_events.randomize_object_pose, + mode="reset", + params={ + "pose_range": { + "x": (0.25, 0.35), + "y": (0.0, 0.10), + "z": (-0.55, -0.55), + "roll": (1.57, 1.57), + "yaw": (-3.14, 3.14), + }, + "asset_cfgs": [SceneEntityCfg("box")], + }, + ) + + +# +# MDP settings +## + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group with state values.""" + + actions = ObsTerm(func=mdp.last_action) + joint_pos = ObsTerm(func=mdp.joint_pos_rel) + joint_vel = ObsTerm(func=mdp.joint_vel_rel) + toy_truck_positions = ObsTerm( + func=place_mdp.object_poses_in_base_frame, + params={"object_cfg": SceneEntityCfg("toy_truck"), "return_key": "pos"}, + ) + toy_truck_orientations = ObsTerm( + func=place_mdp.object_poses_in_base_frame, + params={"object_cfg": SceneEntityCfg("toy_truck"), "return_key": "quat"}, + ) + box_positions = ObsTerm( + func=place_mdp.object_poses_in_base_frame, params={"object_cfg": SceneEntityCfg("box"), "return_key": "pos"} + ) + box_orientations = ObsTerm( + func=place_mdp.object_poses_in_base_frame, + params={"object_cfg": SceneEntityCfg("box"), "return_key": "quat"}, + ) + eef_pos = ObsTerm(func=mdp.ee_frame_pose_in_base_frame, params={"return_key": "pos"}) + eef_quat = ObsTerm(func=mdp.ee_frame_pose_in_base_frame, params={"return_key": "quat"}) + gripper_pos = ObsTerm(func=mdp.gripper_pos) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + @configclass + class SubtaskCfg(ObsGroup): + """Observations for subtask group.""" + + grasp = ObsTerm( + func=place_mdp.object_grasped, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "ee_frame_cfg": SceneEntityCfg("ee_frame"), + "object_cfg": SceneEntityCfg("toy_truck"), + "diff_threshold": 0.05, + }, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + # observation groups + policy: PolicyCfg = PolicyCfg() + subtask_terms: SubtaskCfg = SubtaskCfg() + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + # will be set by agent env cfg + arm_action: mdp.JointPositionActionCfg = MISSING + gripper_action: mdp.BinaryJointPositionActionCfg = MISSING + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + toy_truck_dropping = DoneTerm( + func=mdp.root_height_below_minimum, params={"minimum_height": -0.85, "asset_cfg": SceneEntityCfg("toy_truck")} + ) + + success = DoneTerm( + func=place_mdp.object_a_is_into_b, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "object_a_cfg": SceneEntityCfg("toy_truck"), + "object_b_cfg": SceneEntityCfg("box"), + "xy_threshold": 0.10, + "height_diff": 0.06, + "height_threshold": 0.04, + }, + ) + + +@configclass +class PlaceToy2BoxEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the stacking environment.""" + + # Scene settings + scene: ObjectTableSceneCfg = ObjectTableSceneCfg(num_envs=4096, env_spacing=3.0, replicate_physics=False) + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + # MDP settings + terminations: TerminationsCfg = TerminationsCfg() + + # Unused managers + commands = None + rewards = None + events = None + curriculum = None + + def __post_init__(self): + """Post initialization.""" + + self.sim.render_interval = self.decimation + + self.sim.physx.bounce_threshold_velocity = 0.2 + self.sim.physx.bounce_threshold_velocity = 0.01 + self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4 + self.sim.physx.gpu_total_aggregate_pairs_capacity = 16 * 1024 + self.sim.physx.friction_correlation_distance = 0.00625 + + +""" +Env to Replay Sim2Lab Demonstrations with JointSpaceAction +""" + + +class RmpFlowAgibotPlaceToy2BoxEnvCfg(PlaceToy2BoxEnvCfg): + + def __post_init__(self): + # post init of parent + super().__post_init__() + + self.events = EventCfgPlaceToy2Box() + + # Set Agibot as robot + self.scene.robot = AGIBOT_A2D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + # add table + self.scene.table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0.5, 0.0, -0.7], rot=[0.707, 0, 0, 0.707]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd", + scale=(1.8, 1.0, 0.30), + ), + ) + + use_relative_mode_env = os.getenv("USE_RELATIVE_MODE", "True") + self.use_relative_mode = use_relative_mode_env.lower() in ["true", "1", "t"] + + # Set actions for the specific robot type (Agibot) + self.actions.arm_action = RMPFlowActionCfg( + asset_name="robot", + joint_names=["right_arm_joint.*"], + body_name="right_gripper_center", + controller=AGIBOT_RIGHT_ARM_RMPFLOW_CFG, + scale=1.0, + body_offset=RMPFlowActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.0]), + articulation_prim_expr="/World/envs/env_.*/Robot", + use_relative_mode=self.use_relative_mode, + ) + + # Enable Parallel Gripper: + self.actions.gripper_action = mdp.BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["right_hand_joint1", "right_.*_Support_Joint"], + open_command_expr={"right_hand_joint1": 0.994, "right_.*_Support_Joint": 0.994}, + close_command_expr={"right_hand_joint1": 0.20, "right_.*_Support_Joint": 0.20}, + ) + + # find joint ids for grippers + self.gripper_joint_names = ["right_hand_joint1", "right_Right_1_Joint"] + self.gripper_open_val = 0.994 + self.gripper_threshold = 0.2 + + # Rigid body properties of toy_truck and box + toy_truck_properties = RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=1, + max_angular_velocity=1000.0, + max_linear_velocity=1000.0, + max_depenetration_velocity=5.0, + disable_gravity=False, + ) + + box_properties = RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=1, + max_angular_velocity=1000.0, + max_linear_velocity=1000.0, + max_depenetration_velocity=5.0, + disable_gravity=False, + ) + + # Notes: remember to add Physics/Mass properties to the toy_truck mesh to make grasping successful, + # then you can use below MassPropertiesCfg to set the mass of the toy_truck + toy_mass_properties = MassPropertiesCfg( + mass=0.05, + ) + + self.scene.toy_truck = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/ToyTruck", + init_state=RigidObjectCfg.InitialStateCfg(), + spawn=UsdFileCfg( + usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Objects/toy_truck_022.usd", + scale=(0.001, 0.001, 0.001), + rigid_props=toy_truck_properties, + mass_props=toy_mass_properties, + ), + ) + + self.scene.box = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Box", + init_state=RigidObjectCfg.InitialStateCfg(), + spawn=UsdFileCfg( + usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Objects/box_167.usd", + activate_contact_sensors=True, + scale=(0.001, 0.001, 0.001), + rigid_props=box_properties, + ), + ) + + # Listens to the required transforms + self.marker_cfg = FRAME_MARKER_CFG.copy() + self.marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) + self.marker_cfg.prim_path = "/Visuals/FrameTransformer" + + self.scene.ee_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base_link", + debug_vis=False, + visualizer_cfg=self.marker_cfg, + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/right_gripper_center", + name="end_effector", + offset=OffsetCfg( + pos=[0.0, 0.0, 0.0], + ), + ), + ], + ) + + # add contact force sensor for grasped checking + self.scene.contact_grasp = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/right_.*_Pad_Link", + update_period=0.0, + history_length=6, + debug_vis=True, + filter_prim_paths_expr=["{ENV_REGEX_NS}/ToyTruck/Aligned"], + ) + + self.teleop_devices = DevicesCfg( + devices={ + "keyboard": Se3KeyboardCfg( + pos_sensitivity=0.05, + rot_sensitivity=0.05, + sim_device=self.sim.device, + ), + "spacemouse": Se3SpaceMouseCfg( + pos_sensitivity=0.05, + rot_sensitivity=0.05, + sim_device=self.sim.device, + ), + } + ) + + # Set the simulation parameters + self.sim.dt = 1 / 60 + self.sim.render_interval = 6 + + self.decimation = 3 + self.episode_length_s = 30.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py new file mode 100644 index 00000000000..4426eb423ce --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py @@ -0,0 +1,284 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +from dataclasses import MISSING + +from isaaclab_assets import ISAACLAB_ASSETS_DATA_DIR + +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.devices.device_base import DevicesCfg +from isaaclab.devices.keyboard import Se3KeyboardCfg +from isaaclab.devices.spacemouse import Se3SpaceMouseCfg +from isaaclab.envs.mdp.actions.rmpflow_actions_cfg import RMPFlowActionCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.sensors import ContactSensorCfg, FrameTransformerCfg +from isaaclab.sensors.frame_transformer.frame_transformer_cfg import OffsetCfg +from isaaclab.sim.schemas.schemas_cfg import RigidBodyPropertiesCfg +from isaaclab.sim.spawners.from_files.from_files_cfg import UsdFileCfg +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from isaaclab_tasks.manager_based.manipulation.place import mdp as place_mdp +from isaaclab_tasks.manager_based.manipulation.place.config.agibot import place_toy2box_rmp_rel_env_cfg +from isaaclab_tasks.manager_based.manipulation.stack import mdp +from isaaclab_tasks.manager_based.manipulation.stack.mdp import franka_stack_events + +## +# Pre-defined configs +## +from isaaclab.markers.config import FRAME_MARKER_CFG # isort: skip +from isaaclab_assets.robots.agibot import AGIBOT_A2D_CFG # isort: skip +from isaaclab.controllers.config.rmp_flow import AGIBOT_LEFT_ARM_RMPFLOW_CFG # isort: skip + +## +# Event settings +## + + +@configclass +class EventCfgPlaceUprightMug: + """Configuration for events.""" + + reset_all = EventTerm(func=mdp.reset_scene_to_default, mode="reset", params={"reset_joint_targets": True}) + + randomize_mug_positions = EventTerm( + func=franka_stack_events.randomize_object_pose, + mode="reset", + params={ + "pose_range": { + "x": (-0.05, 0.2), + "y": (-0.10, 0.10), + "z": (0.75, 0.75), + "roll": (-1.57, -1.57), + "yaw": (-0.57, 0.57), + }, + "asset_cfgs": [SceneEntityCfg("mug")], + }, + ) + + +# +# MDP settings +## + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group with state values.""" + + actions = ObsTerm(func=mdp.last_action) + joint_pos = ObsTerm(func=mdp.joint_pos_rel) + joint_vel = ObsTerm(func=mdp.joint_vel_rel) + mug_positions = ObsTerm( + func=place_mdp.object_poses_in_base_frame, params={"object_cfg": SceneEntityCfg("mug"), "return_key": "pos"} + ) + mug_orientations = ObsTerm( + func=place_mdp.object_poses_in_base_frame, + params={"object_cfg": SceneEntityCfg("mug"), "return_key": "quat"}, + ) + eef_pos = ObsTerm(func=mdp.ee_frame_pose_in_base_frame, params={"return_key": "pos"}) + eef_quat = ObsTerm(func=mdp.ee_frame_pose_in_base_frame, params={"return_key": "quat"}) + gripper_pos = ObsTerm(func=mdp.gripper_pos) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + @configclass + class SubtaskCfg(ObsGroup): + """Observations for subtask group.""" + + grasp = ObsTerm( + func=place_mdp.object_grasped, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "ee_frame_cfg": SceneEntityCfg("ee_frame"), + "object_cfg": SceneEntityCfg("mug"), + "diff_threshold": 0.05, + }, + ) + + def __post_init__(self): + self.enable_corruption = False + self.concatenate_terms = False + + # observation groups + policy: PolicyCfg = PolicyCfg() + subtask_terms: SubtaskCfg = SubtaskCfg() + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + # will be set by agent env cfg + arm_action: mdp.JointPositionActionCfg = MISSING + gripper_action: mdp.BinaryJointPositionActionCfg = MISSING + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + + mug_dropping = DoneTerm( + func=mdp.root_height_below_minimum, params={"minimum_height": -0.85, "asset_cfg": SceneEntityCfg("mug")} + ) + + success = DoneTerm( + func=place_mdp.object_placed_upright, + params={ + "robot_cfg": SceneEntityCfg("robot"), + "object_cfg": SceneEntityCfg("mug"), + "target_height": 0.6, + }, + ) + + +""" +Env to Place Upright Mug with AgiBot Left Arm using RMPFlow +""" + + +class RmpFlowAgibotPlaceUprightMugEnvCfg(place_toy2box_rmp_rel_env_cfg.PlaceToy2BoxEnvCfg): + + def __post_init__(self): + # post init of parent + super().__post_init__() + + self.events = EventCfgPlaceUprightMug() + + # Set Agibot as robot + self.scene.robot = AGIBOT_A2D_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + self.scene.robot.init_state.pos = (-0.60, 0.0, 0.0) + + # reset obs and termination terms + self.observations = ObservationsCfg() + self.terminations = TerminationsCfg() + + # Table + self.scene.table = AssetBaseCfg( + prim_path="{ENV_REGEX_NS}/Table", + init_state=AssetBaseCfg.InitialStateCfg(pos=[0.50, 0.0, 0.60], rot=[0.707, 0, 0, 0.707]), + spawn=UsdFileCfg( + usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd", + scale=(1.0, 1.0, 0.60), + ), + ) + + # add contact force sensor for grasped checking + self.scene.contact_grasp = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/right_.*_Pad_Link", + update_period=0.0, + history_length=6, + debug_vis=True, + filter_prim_paths_expr=["{ENV_REGEX_NS}/Mug"], + ) + + use_relative_mode_env = os.getenv("USE_RELATIVE_MODE", "True") + self.use_relative_mode = use_relative_mode_env.lower() in ["true", "1", "t"] + + # Set actions for the specific robot type (Agibot) + self.actions.arm_action = RMPFlowActionCfg( + asset_name="robot", + joint_names=["left_arm_joint.*"], + body_name="gripper_center", + controller=AGIBOT_LEFT_ARM_RMPFLOW_CFG, + scale=1.0, + body_offset=RMPFlowActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.0], rot=[0.7071, 0.0, -0.7071, 0.0]), + articulation_prim_expr="/World/envs/env_.*/Robot", + use_relative_mode=self.use_relative_mode, + ) + + # Enable Parallel Gripper: + self.actions.gripper_action = mdp.BinaryJointPositionActionCfg( + asset_name="robot", + joint_names=["left_hand_joint1", "left_.*_Support_Joint"], + open_command_expr={"left_hand_joint1": 0.994, "left_.*_Support_Joint": 0.994}, + close_command_expr={"left_hand_joint1": 0.0, "left_.*_Support_Joint": 0.0}, + ) + + # find joint ids for grippers + self.gripper_joint_names = ["left_hand_joint1", "left_Right_1_Joint"] + self.gripper_open_val = 0.994 + self.gripper_threshold = 0.2 + + # Rigid body properties of mug + mug_properties = RigidBodyPropertiesCfg( + solver_position_iteration_count=16, + solver_velocity_iteration_count=1, + max_angular_velocity=1000.0, + max_linear_velocity=1000.0, + max_depenetration_velocity=5.0, + disable_gravity=False, + ) + + self.scene.mug = RigidObjectCfg( + prim_path="{ENV_REGEX_NS}/Mug", + init_state=RigidObjectCfg.InitialStateCfg(), + spawn=UsdFileCfg( + usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Objects/mug.usd", + scale=(1.0, 1.0, 1.0), + rigid_props=mug_properties, + ), + ) + + # Listens to the required transforms + self.marker_cfg = FRAME_MARKER_CFG.copy() + self.marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) + self.marker_cfg.prim_path = "/Visuals/FrameTransformer" + + self.scene.ee_frame = FrameTransformerCfg( + prim_path="{ENV_REGEX_NS}/Robot/base_link", + debug_vis=False, + visualizer_cfg=self.marker_cfg, + target_frames=[ + FrameTransformerCfg.FrameCfg( + prim_path="{ENV_REGEX_NS}/Robot/gripper_center", + name="end_effector", + offset=OffsetCfg( + pos=[0.0, 0.0, 0.0], + rot=[ + 0.7071, + 0.0, + -0.7071, + 0.0, + ], + ), + ), + ], + ) + + self.teleop_devices = DevicesCfg( + devices={ + "keyboard": Se3KeyboardCfg( + pos_sensitivity=0.05, + rot_sensitivity=0.05, + sim_device=self.sim.device, + ), + "spacemouse": Se3SpaceMouseCfg( + pos_sensitivity=0.05, + rot_sensitivity=0.05, + sim_device=self.sim.device, + ), + } + ) + + # Set the simulation parameters + self.sim.dt = 1 / 60 + self.sim.render_interval = 6 + + self.decimation = 3 + self.episode_length_s = 10.0 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py new file mode 100644 index 00000000000..f394d204c70 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""This sub-module contains the functions that are specific to the pick and place environments.""" + +from isaaclab.envs.mdp import * # noqa: F401, F403 + +from .observations import * # noqa: F401, F403 +from .terminations import * # noqa: F401, F403 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py new file mode 100644 index 00000000000..18870db2cad --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/observations.py @@ -0,0 +1,118 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING, Literal + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import FrameTransformer + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def object_poses_in_base_frame( + env: ManagerBasedRLEnv, + object_cfg: SceneEntityCfg = SceneEntityCfg("mug"), + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + return_key: Literal["pos", "quat", None] = None, +) -> torch.Tensor: + """The pose of the object in the robot base frame.""" + object: RigidObject = env.scene[object_cfg.name] + + pos_object_world = object.data.root_pos_w + quat_object_world = object.data.root_quat_w + + """The position of the robot in the world frame.""" + robot: Articulation = env.scene[robot_cfg.name] + root_pos_w = robot.data.root_pos_w + root_quat_w = robot.data.root_quat_w + + pos_object_base, quat_object_base = math_utils.subtract_frame_transforms( + root_pos_w, root_quat_w, pos_object_world, quat_object_world + ) + if return_key == "pos": + return pos_object_base + elif return_key == "quat": + return quat_object_base + elif return_key is None: + return torch.cat((pos_object_base, quat_object_base), dim=1) + + +def object_grasped( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg, + ee_frame_cfg: SceneEntityCfg, + object_cfg: SceneEntityCfg, + diff_threshold: float = 0.06, + force_threshold: float = 1.0, +) -> torch.Tensor: + """ + Check if an object is grasped by the specified robot. + Support both surface gripper and parallel gripper. + If contact_grasp sensor is found, check if the contact force is greater than force_threshold. + """ + + robot: Articulation = env.scene[robot_cfg.name] + ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + + object_pos = object.data.root_pos_w + end_effector_pos = ee_frame.data.target_pos_w[:, 0, :] + pose_diff = torch.linalg.vector_norm(object_pos - end_effector_pos, dim=1) + + if "contact_grasp" in env.scene.keys() and env.scene["contact_grasp"] is not None: + contact_force_grasp = env.scene["contact_grasp"].data.net_forces_w # shape:(N, 2, 3) for two fingers + contact_force_norm = torch.linalg.vector_norm( + contact_force_grasp, dim=2 + ) # shape:(N, 2) - force magnitude per finger + both_fingers_force_ok = torch.all( + contact_force_norm > force_threshold, dim=1 + ) # both fingers must exceed threshold + grasped = torch.logical_and(pose_diff < diff_threshold, both_fingers_force_ok) + elif ( + f"contact_grasp_{object_cfg.name}" in env.scene.keys() + and env.scene[f"contact_grasp_{object_cfg.name}"] is not None + ): + contact_force_object = env.scene[ + f"contact_grasp_{object_cfg.name}" + ].data.net_forces_w # shape:(N, 2, 3) for two fingers + contact_force_norm = torch.linalg.vector_norm( + contact_force_object, dim=2 + ) # shape:(N, 2) - force magnitude per finger + both_fingers_force_ok = torch.all( + contact_force_norm > force_threshold, dim=1 + ) # both fingers must exceed threshold + grasped = torch.logical_and(pose_diff < diff_threshold, both_fingers_force_ok) + else: + grasped = (pose_diff < diff_threshold).clone().detach() + + if hasattr(env.scene, "surface_grippers") and len(env.scene.surface_grippers) > 0: + surface_gripper = env.scene.surface_grippers["surface_gripper"] + suction_cup_status = surface_gripper.state.view(-1, 1) # 1: closed, 0: closing, -1: open + suction_cup_is_closed = (suction_cup_status == 1).to(torch.float32) + grasped = torch.logical_and(suction_cup_is_closed, pose_diff < diff_threshold) + + else: + if hasattr(env.cfg, "gripper_joint_names"): + gripper_joint_ids, _ = robot.find_joints(env.cfg.gripper_joint_names) + grasped = torch.logical_and( + grasped, + torch.abs(torch.abs(robot.data.joint_pos[:, gripper_joint_ids[0]]) - env.cfg.gripper_open_val) + > env.cfg.gripper_threshold, + ) + grasped = torch.logical_and( + grasped, + torch.abs(torch.abs(robot.data.joint_pos[:, gripper_joint_ids[1]]) - env.cfg.gripper_open_val) + > env.cfg.gripper_threshold, + ) + else: + raise ValueError("No gripper_joint_names found in environment config") + + return grasped diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py new file mode 100644 index 00000000000..9768321ef13 --- /dev/null +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/mdp/terminations.py @@ -0,0 +1,122 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Common functions that can be used to activate certain terminations for the place task. + +The functions can be passed to the :class:`isaaclab.managers.TerminationTermCfg` object to enable +the termination introduced by the function. +""" + +from __future__ import annotations + +import torch +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from isaaclab.envs import ManagerBasedRLEnv + + +def object_placed_upright( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg, + object_cfg: SceneEntityCfg, + target_height: float = 0.927, + euler_xy_threshold: float = 0.10, +): + """Check if an object placed upright by the specified robot.""" + + robot: Articulation = env.scene[robot_cfg.name] + object: RigidObject = env.scene[object_cfg.name] + + # Compute mug euler angles of X, Y axis, to check if it is placed upright + object_euler_x, object_euler_y, _ = math_utils.euler_xyz_from_quat(object.data.root_quat_w) # (N,4) [0, 2*pi] + + object_euler_x_err = torch.abs(math_utils.wrap_to_pi(object_euler_x)) # (N,) + object_euler_y_err = torch.abs(math_utils.wrap_to_pi(object_euler_y)) # (N,) + + success = torch.logical_and(object_euler_x_err < euler_xy_threshold, object_euler_y_err < euler_xy_threshold) + + # Check if current mug height is greater than target height + height_success = object.data.root_pos_w[:, 2] > target_height + + success = torch.logical_and(height_success, success) + + if hasattr(env.scene, "surface_grippers") and len(env.scene.surface_grippers) > 0: + surface_gripper = env.scene.surface_grippers["surface_gripper"] + suction_cup_status = surface_gripper.state.view(-1, 1) # 1: closed, 0: closing, -1: open + suction_cup_is_open = (suction_cup_status == -1).to(torch.float32) + success = torch.logical_and(suction_cup_is_open, success) + + else: + if hasattr(env.cfg, "gripper_joint_names"): + gripper_joint_ids, _ = robot.find_joints(env.cfg.gripper_joint_names) + success = torch.logical_and( + success, + torch.abs(torch.abs(robot.data.joint_pos[:, gripper_joint_ids[0]]) - env.cfg.gripper_open_val) + < env.cfg.gripper_threshold, + ) + success = torch.logical_and( + success, + torch.abs(torch.abs(robot.data.joint_pos[:, gripper_joint_ids[1]]) - env.cfg.gripper_open_val) + < env.cfg.gripper_threshold, + ) + else: + raise ValueError("No gripper_joint_names found in environment config") + + return success + + +def object_a_is_into_b( + env: ManagerBasedRLEnv, + robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"), + object_a_cfg: SceneEntityCfg = SceneEntityCfg("object_a"), + object_b_cfg: SceneEntityCfg = SceneEntityCfg("object_b"), + xy_threshold: float = 0.03, # xy_distance_threshold + height_threshold: float = 0.04, # height_distance_threshold + height_diff: float = 0.0, # expected height_diff +) -> torch.Tensor: + """Check if an object a is put into another object b by the specified robot.""" + + robot: Articulation = env.scene[robot_cfg.name] + object_a: RigidObject = env.scene[object_a_cfg.name] + object_b: RigidObject = env.scene[object_b_cfg.name] + + # check object a is into object b + pos_diff = object_a.data.root_pos_w - object_b.data.root_pos_w + height_dist = torch.linalg.vector_norm(pos_diff[:, 2:], dim=1) + xy_dist = torch.linalg.vector_norm(pos_diff[:, :2], dim=1) + + success = torch.logical_and(xy_dist < xy_threshold, (height_dist - height_diff) < height_threshold) + + # Check gripper positions + if hasattr(env.scene, "surface_grippers") and len(env.scene.surface_grippers) > 0: + surface_gripper = env.scene.surface_grippers["surface_gripper"] + suction_cup_status = surface_gripper.state.view(-1, 1) # 1: closed, 0: closing, -1: open + suction_cup_is_open = (suction_cup_status == -1).to(torch.float32) + success = torch.logical_and(suction_cup_is_open, success) + + else: + if hasattr(env.cfg, "gripper_joint_names"): + gripper_joint_ids, _ = robot.find_joints(env.cfg.gripper_joint_names) + assert len(gripper_joint_ids) == 2, "Terminations only support parallel gripper for now" + + success = torch.logical_and( + success, + torch.abs(torch.abs(robot.data.joint_pos[:, gripper_joint_ids[0]]) - env.cfg.gripper_open_val) + < env.cfg.gripper_threshold, + ) + success = torch.logical_and( + success, + torch.abs(torch.abs(robot.data.joint_pos[:, gripper_joint_ids[1]]) - env.cfg.gripper_open_val) + < env.cfg.gripper_threshold, + ) + else: + raise ValueError("No gripper_joint_names found in environment config") + + return success From 2a4e165ec5ebc0abbf47b805013c1e1440ff95d1 Mon Sep 17 00:00:00 2001 From: Alexander Poddubny <143108850+nv-apoddubny@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:27:53 -0700 Subject: [PATCH 13/50] Fixes CI to fail the job for a fork PRs when general tests fail (#3412) # Description Fail the pre-merge CI job for a fork PRs when general tests fail - Add step to check test results for PRs from forks - Parse XML test report to detect failures/errors - Fail job immediately if tests don't pass for fork PRs - Maintain existing behavior for same-repo PRs ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .github/workflows/build.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89c6501a2ee..7a93264c2ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,11 +121,19 @@ jobs: retention-days: 1 compression-level: 9 - - name: Fail on Test Failure for Fork PRs - if: github.event.pull_request.head.repo.full_name != github.repository && steps.run-general-tests.outcome == 'failure' + - name: Check Test Results for Fork PRs + if: github.event.pull_request.head.repo.full_name != github.repository run: | - echo "Tests failed for PR from fork. The test report is in the logs. Failing the job." - exit 1 + if [ -f "reports/general-tests-report.xml" ]; then + # Check if the test results contain any failures + if grep -q 'failures="[1-9]' reports/general-tests-report.xml || grep -q 'errors="[1-9]' reports/general-tests-report.xml; then + echo "Tests failed for PR from fork. The test report is in the logs. Failing the job." + exit 1 + fi + else + echo "No test results file found. This might indicate test execution failed." + exit 1 + fi combine-results: needs: [test-isaaclab-tasks, test-general] From b3c8afaa71c27dfcf21fc39d1aef94db7a8c9571 Mon Sep 17 00:00:00 2001 From: klakhi Date: Tue, 9 Sep 2025 21:45:27 -0700 Subject: [PATCH 14/50] Adding help str to args --- tools/template/non_interactive.py | 45 +++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py index e039b4c06d6..3b7b39095f7 100644 --- a/tools/template/non_interactive.py +++ b/tools/template/non_interactive.py @@ -54,15 +54,46 @@ def main(argv: list[str] | None = None) -> None: _all_algos_map = get_algorithms_per_rl_library(True, True) rl_algo_choices = sorted({algo.lower() for algos in _all_algos_map.values() for algo in algos}) - parser.add_argument("--task-type", "--task_type", type=str, required=True, choices=["External", "Internal"]) - parser.add_argument("--project-path", "--project_path", type=str) - parser.add_argument("--project-name", "--project_name", type=str, required=True) + parser.add_argument( + "--task-type", + "--task_type", + type=str, + required=True, + choices=["External", "Internal"], + help=( + "Where to create the project: 'External' (requires --project-path and must be outside this repo) " + "or 'Internal' (generated within the Isaac Lab repo)." + ), + ) + parser.add_argument( + "--project-path", + "--project_path", + type=str, + help=( + "Destination path for an external project. Required when --task-type External. " + "Must not be within the Isaac Lab project." + ), + ) + parser.add_argument( + "--project-name", + "--project_name", + type=str, + required=True, + help=( + "Project identifier used in generated files (letters, digits, underscores)" + ), + ) parser.add_argument( "--workflow", action="append", required=True, type=str.lower, choices=[*([w.lower() for w in supported_workflows]), "all"], + help=( + "Workflow(s) to generate. Repeat this flag to include multiple, or use 'all'. " + "Allowed values: direct_single_agent, direct_multi_agent, manager-based_single_agent. " + "Values are case-insensitive; underscores in the type are normalized (e.g., single_agent → single-agent)." + ), ) parser.add_argument( "--rl-library", @@ -70,6 +101,10 @@ def main(argv: list[str] | None = None) -> None: type=str.lower, required=True, choices=[*supported_rl_libraries, "all"], + help=( + "RL library to target or 'all'. Choices are filtered by the selected workflows; libraries without " + "supported algorithms under those workflows are omitted." + ), ) parser.add_argument( "--rl-algorithm", @@ -79,8 +114,8 @@ def main(argv: list[str] | None = None) -> None: default=None, choices=[*rl_algo_choices, "all"], help=( - "RL algorithm to use. If omitted, the tool auto-selects when exactly one algorithm " - "is valid for the chosen workflows and library." + "RL algorithm to use. If skipped, auto-selects when exactly one algorithm is valid for the chosen " + "workflows and library. Use 'all' to include every supported algorithm per selected library." ), ) From 34f18c9786e4e977fe604f308a47435e213e5464 Mon Sep 17 00:00:00 2001 From: klakhi Date: Wed, 10 Sep 2025 04:50:59 +0000 Subject: [PATCH 15/50] format changeS --- tools/template/non_interactive.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py index 3b7b39095f7..cfe22615b80 100644 --- a/tools/template/non_interactive.py +++ b/tools/template/non_interactive.py @@ -79,9 +79,7 @@ def main(argv: list[str] | None = None) -> None: "--project_name", type=str, required=True, - help=( - "Project identifier used in generated files (letters, digits, underscores)" - ), + help="Project identifier used in generated files (letters, digits, underscores)", ) parser.add_argument( "--workflow", From 2b2e0c55c65e7fa8b3048fd82d7d1c16160f42d8 Mon Sep 17 00:00:00 2001 From: klakhi Date: Tue, 9 Sep 2025 21:52:49 -0700 Subject: [PATCH 16/50] Adding help section --- tools/template/non_interactive.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/template/non_interactive.py b/tools/template/non_interactive.py index 3b7b39095f7..6c26ea528da 100644 --- a/tools/template/non_interactive.py +++ b/tools/template/non_interactive.py @@ -43,7 +43,13 @@ def main(argv: list[str] | None = None) -> None: This avoids any interactive prompts or dependencies on Inquirer-based flow. """ - parser = argparse.ArgumentParser(add_help=False) + parser = argparse.ArgumentParser( + description=( + "Non-interactive template generator for Isaac Lab. Use flags to choose workflows, RL libraries, " + "and algorithms." + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) supported_workflows = [ "direct_single_agent", "direct_multi_agent", From 817239026650e5b75946b49c7ab6afd74ceb0756 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:28:11 +0200 Subject: [PATCH 17/50] Improves contribution guidelines for IsaacLab (#3403) # Description I realized there were some comments I often have to repeat in my reviewing process. I tried to add some of them into the code style page to directly point developers to it. ## Type of change - This change requires a documentation update ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Co-authored-by: James Tigue <166445701+jtigue-bdai@users.noreply.github.com> --- .github/PULL_REQUEST_TEMPLATE.md | 7 +- docs/conf.py | 3 +- docs/source/overview/environments.rst | 2 - docs/source/refs/contributing.rst | 112 ++++++++++++++- docs/source/refs/snippets/code_skeleton.py | 155 +++++++++++++++++++++ 5 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 docs/source/refs/snippets/code_skeleton.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e9176cc47f5..ee9fa4ebdc5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,8 @@ Thank you for your interest in sending a pull request. Please make sure to check the contribution guidelines. Link: https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html + +💡 Please try to keep PRs small and focused. Large PRs are harder to review and merge. --> Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. @@ -21,8 +23,8 @@ is demanded by more than one party. --> - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) -- Breaking change (fix or feature that would cause existing functionality to not work as expected) -- This change requires a documentation update +- Breaking change (existing functionality will not work without user modification) +- Documentation update ## Screenshots @@ -40,6 +42,7 @@ To upload images to a PR -- simply drag and drop an image while in edit mode and ## Checklist +- [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings diff --git a/docs/conf.py b/docs/conf.py index 3bdf99666ed..28580574b26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -122,7 +122,7 @@ "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), "trimesh": ("https://trimesh.org/", None), - "torch": ("https://pytorch.org/docs/stable/", None), + "torch": ("https://docs.pytorch.org/docs/stable", None), "isaacsim": ("https://docs.isaacsim.omniverse.nvidia.com/5.0.0/py/", None), "gymnasium": ("https://gymnasium.farama.org/", None), "warp": ("https://nvidia.github.io/warp/", None), @@ -162,6 +162,7 @@ "isaacsim.core.api", "isaacsim.core.cloner", "isaacsim.core.version", + "isaacsim.core.utils", "isaacsim.robot_motion.motion_generation", "isaacsim.gui.components", "isaacsim.asset.importer.urdf", diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index 39431041c1f..72dcbff1851 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -190,12 +190,10 @@ for the lift-cube environment: .. |galbot_stack-link| replace:: `Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 `__ .. |kuka-allegro-lift-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Lift-v0 `__ .. |kuka-allegro-reorient-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 `__ - .. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ .. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ .. |cube-shadow-lstm-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 `__ .. |cube-shadow-vis-link| replace:: `Isaac-Repose-Cube-Shadow-Vision-Direct-v0 `__ - .. |agibot_place_mug-link| replace:: `Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0 `__ .. |agibot_place_toy-link| replace:: `Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 `__ diff --git a/docs/source/refs/contributing.rst b/docs/source/refs/contributing.rst index f4bc45003b7..40c84619655 100644 --- a/docs/source/refs/contributing.rst +++ b/docs/source/refs/contributing.rst @@ -54,6 +54,9 @@ Please ensure that your code is well-formatted, documented and passes all the te Large pull requests are difficult to review and may take a long time to merge. +More details on the code style and testing can be found in the `Coding Style`_ and `Unit Testing`_ sections. + + Contributing Documentation -------------------------- @@ -237,6 +240,62 @@ For documentation, we adopt the `Google Style Guide `__ for generating the documentation. Please make sure that your code is well-documented and follows the guidelines. +Code Structure +^^^^^^^^^^^^^^ + +We follow a specific structure for the codebase. This helps in maintaining the codebase and makes it easier to +understand. + +In a Python file, we follow the following structure: + +.. code:: python + + # Imports: These are sorted by the pre-commit hooks. + # Constants + # Functions (public) + # Classes (public) + # _Functions (private) + # _Classes (private) + +Imports are sorted by the pre-commit hooks. Unless there is a good reason to do otherwise, please do not +import the modules inside functions or classes. To deal with circular imports, we use the +:obj:`typing.TYPE_CHECKING` variable. Please refer to the `Circular Imports`_ section for more details. + +Python does not have a concept of private and public classes and functions. However, we follow the +convention of prefixing the private functions and classes with an underscore. +The public functions and classes are the ones that are intended to be used by the users. The private +functions and classes are the ones that are intended to be used internally in that file. +Irrespective of the public or private nature of the functions and classes, we follow the Style Guide +for the code and make sure that the code and documentation are consistent. + +Similarly, within Python classes, we follow the following structure: + +.. code:: python + + # Constants + # Class variables (public or private): Must have the type hint ClassVar[type] + # Dunder methods: __init__, __del__ + # Representation: __repr__, __str__ + # Properties: @property + # Instance methods (public) + # Class methods (public) + # Static methods (public) + # _Instance methods (private) + # _Class methods (private) + # _Static methods (private) + +The rule of thumb is that the functions within the classes are ordered in the way a user would +expect to use them. For instance, if the class contains the method :meth:`initialize`, :meth:`reset`, +:meth:`update`, and :meth:`close`, then they should be listed in the order of their usage. +The same applies for private functions in the class. Their order is based on the order of call inside the +class. + +.. dropdown:: Code skeleton + :icon: code + + .. literalinclude:: snippets/code_skeleton.py + :language: python + Circular Imports ^^^^^^^^^^^^^^^^ @@ -414,15 +473,47 @@ We summarize the key points below: Unit Testing -^^^^^^^^^^^^ +------------ We use `pytest `__ for unit testing. Good tests not only cover the basic functionality of the code but also the edge cases. They should be able to catch regressions and ensure that the code is working as expected. Please make sure that you add tests for your changes. +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # Run all tests + ./isaaclab.sh --test # or "./isaaclab.sh -t" + + # Run all tests in a particular file + ./isaaclab.sh -p -m pytest source/isaaclab/test/deps/test_torch.py + + # Run a particular test + ./isaaclab.sh -p -m pytest source/isaaclab/test/deps/test_torch.py::test_array_slicing + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: bash + + # Run all tests + isaaclab.bat --test # or "isaaclab.bat -t" + + # Run all tests in a particular file + isaaclab.bat -p -m pytest source/isaaclab/test/deps/test_torch.py + + # Run a particular test + isaaclab.bat -p -m pytest source/isaaclab/test/deps/test_torch.py::test_array_slicing + + Tools -^^^^^ +----- We use the following tools for maintaining code quality: @@ -435,6 +526,19 @@ Please check `here `__ for instructions to set these up. To run over the entire repository, please execute the following command in the terminal: -.. code:: bash +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + ./isaaclab.sh --format # or "./isaaclab.sh -f" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: bash - ./isaaclab.sh --format # or "./isaaclab.sh -f" + isaaclab.bat --format # or "isaaclab.bat -f" diff --git a/docs/source/refs/snippets/code_skeleton.py b/docs/source/refs/snippets/code_skeleton.py new file mode 100644 index 00000000000..cf0385279b6 --- /dev/null +++ b/docs/source/refs/snippets/code_skeleton.py @@ -0,0 +1,155 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import sys +from typing import ClassVar + + +DEFAULT_TIMEOUT: int = 30 +"""Default timeout for the task.""" + +_MAX_RETRIES: int = 3 # private constant (note the underscore) +"""Maximum number of retries for the task.""" + + +def run_task(task_name: str): + """Run a task by name. + + Args: + task_name: The name of the task to run. + """ + print(f"Running task: {task_name}") + + +class TaskRunner: + """Runs and manages tasks.""" + + DEFAULT_NAME: ClassVar[str] = "runner" + """Default name for the runner.""" + + _registry: ClassVar[dict] = {} + """Registry of runners.""" + + def __init__(self, name: str): + """Initialize the runner. + + Args: + name: The name of the runner. + """ + self.name = name + self._tasks = [] # private instance variable + + def __del__(self): + """Clean up the runner.""" + print(f"Cleaning up {self.name}") + + def __repr__(self) -> str: + return f"TaskRunner(name={self.name!r})" + + def __str__(self) -> str: + return f"TaskRunner: {self.name}" + + """ + Properties. + """ + + @property + def task_count(self) -> int: + return len(self._tasks) + + """ + Operations. + """ + + def initialize(self): + """Initialize the runner.""" + print("Initializing runner...") + + def update(self, task: str): + """Update the runner with a new task. + + Args: + task: The task to add. + """ + self._tasks.append(task) + print(f"Added task: {task}") + + def close(self): + """Close the runner.""" + print("Closing runner...") + + """ + Operations: Registration. + """ + + @classmethod + def register(cls, name: str, runner: "TaskRunner"): + """Register a runner. + + Args: + name: The name of the runner. + runner: The runner to register. + """ + if name in cls._registry: + _log_error(f"Runner {name} already registered. Skipping registration.") + return + cls._registry[name] = runner + + @staticmethod + def validate_task(task: str) -> bool: + """Validate a task. + + Args: + task: The task to validate. + + Returns: + True if the task is valid, False otherwise. + """ + return bool(task and task.strip()) + + """ + Internal operations. + """ + + def _reset(self): + """Reset the runner.""" + self._tasks.clear() + + @classmethod + def _get_registry(cls) -> dict: + """Get the registry.""" + return cls._registry + + @staticmethod + def _internal_helper(): + """Internal helper.""" + print("Internal helper called.") + + +""" +Helper operations. +""" + + +def _log_error(message: str): + """Internal helper to log errors. + + Args: + message: The message to log. + """ + print(f"[ERROR] {message}") + + +class _TaskHelper: + """Private utility class for internal task logic.""" + + def compute(self) -> int: + """Compute the result. + + Returns: + The result of the computation. + """ + return 42 From 4b56b4ffd8dc180ccef77098801336f5ee9e0269 Mon Sep 17 00:00:00 2001 From: PeterL-NV Date: Tue, 9 Sep 2025 23:46:18 -0700 Subject: [PATCH 18/50] Corrects materials and objects imports in `check_terrain_importer.py` (#3411) # Description Update namespace for materials and objects in the terrain generator test. Fixes #3383 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- source/isaaclab/test/terrains/check_terrain_importer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/isaaclab/test/terrains/check_terrain_importer.py b/source/isaaclab/test/terrains/check_terrain_importer.py index 950d3d624ef..2de8b457e32 100644 --- a/source/isaaclab/test/terrains/check_terrain_importer.py +++ b/source/isaaclab/test/terrains/check_terrain_importer.py @@ -67,10 +67,11 @@ import isaacsim.core.utils.prims as prim_utils import omni.kit import omni.kit.commands +from isaacsim.core.api.materials import PhysicsMaterial +from isaacsim.core.api.materials.preview_surface import PreviewSurface +from isaacsim.core.api.objects import DynamicSphere from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.cloner import GridCloner -from isaacsim.core.materials import PhysicsMaterial, PreviewSurface -from isaacsim.core.objects import DynamicSphere from isaacsim.core.prims import RigidPrim, SingleGeometryPrim, SingleRigidPrim from isaacsim.core.utils.extensions import enable_extension from isaacsim.core.utils.viewports import set_camera_view From 9d501c3ba4fc2ba3cd3290d40cc07356758414b7 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Thu, 11 Sep 2025 00:11:57 +0200 Subject: [PATCH 19/50] Adds parsing of instanced meshes to fetching prims utils (#3367) # Description This MR does the following: * Adds parsing of instanced prims in `isaaclab.sim.utils.get_all_matching_child_prims` and `isaaclab.sim.utils.get_first_matching_child_prim`. Earlier, instanced prims were skipped since `Usd.Prim.GetChildren` does not return instanced prims. * Adds parsing of instanced prims in `isaaclab.sim.utils.make_uninstanceable` to make all prims uninstanceable. These are needed to parse meshes for the dynamic raycaster class in #3298 ## Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) - Breaking change (fix or feature that would cause existing functionality to not work as expected) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Co-authored-by: Kelly Guo --- source/isaaclab/config/extension.toml | 2 +- source/isaaclab/docs/CHANGELOG.rst | 20 +++++++ .../assets/articulation/articulation.py | 1 + .../deformable_object/deformable_object.py | 4 +- .../assets/rigid_object/rigid_object.py | 8 ++- .../rigid_object_collection.py | 8 ++- .../assets/surface_gripper/surface_gripper.py | 4 +- source/isaaclab/isaaclab/sim/utils.py | 52 +++++++++++++++-- source/isaaclab/test/sim/test_utils.py | 56 +++++++++++++++++-- 9 files changed, 138 insertions(+), 17 deletions(-) diff --git a/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml index a53b7e970cb..9d22bb9f692 100644 --- a/source/isaaclab/config/extension.toml +++ b/source/isaaclab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.45.15" +version = "0.46.0" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index 91d5e1ab1ed..c8eaee1d724 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -1,6 +1,26 @@ Changelog --------- +0.46.0 (2025-09-06) +~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added argument :attr:`traverse_instance_prims` to :meth:`~isaaclab.sim.utils.get_all_matching_child_prims` and + :meth:`~isaaclab.sim.utils.get_first_matching_child_prim` to control whether to traverse instance prims + during the traversal. Earlier, instanced prims were skipped since :meth:`Usd.Prim.GetChildren` did not return + instanced prims, which is now fixed. + + +Changed +^^^^^^^ + +* Made parsing of instanced prims in :meth:`~isaaclab.sim.utils.get_all_matching_child_prims` and + :meth:`~isaaclab.sim.utils.get_first_matching_child_prim` as the default behavior. +* Added parsing of instanced prims in :meth:`~isaaclab.sim.utils.make_uninstanceable` to make all prims uninstanceable. + + 0.45.15 (2025-09-05) ~~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab/isaaclab/assets/articulation/articulation.py b/source/isaaclab/isaaclab/assets/articulation/articulation.py index 55f3e650ed0..3a720de5573 100644 --- a/source/isaaclab/isaaclab/assets/articulation/articulation.py +++ b/source/isaaclab/isaaclab/assets/articulation/articulation.py @@ -1458,6 +1458,7 @@ def _initialize_impl(self): first_env_root_prims = sim_utils.get_all_matching_child_prims( first_env_matching_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI), + traverse_instance_prims=False, ) if len(first_env_root_prims) == 0: raise RuntimeError( diff --git a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py index 05211af0d2a..982b2f72c81 100644 --- a/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py +++ b/source/isaaclab/isaaclab/assets/deformable_object/deformable_object.py @@ -272,7 +272,9 @@ def _initialize_impl(self): # find deformable root prims root_prims = sim_utils.get_all_matching_child_prims( - template_prim_path, predicate=lambda prim: prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI) + template_prim_path, + predicate=lambda prim: prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI), + traverse_instance_prims=False, ) if len(root_prims) == 0: raise RuntimeError( diff --git a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py index ac76326116e..9de2a137636 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py +++ b/source/isaaclab/isaaclab/assets/rigid_object/rigid_object.py @@ -464,7 +464,9 @@ def _initialize_impl(self): # find rigid root prims root_prims = sim_utils.get_all_matching_child_prims( - template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI) + template_prim_path, + predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI), + traverse_instance_prims=False, ) if len(root_prims) == 0: raise RuntimeError( @@ -479,7 +481,9 @@ def _initialize_impl(self): ) articulation_prims = sim_utils.get_all_matching_child_prims( - template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + template_prim_path, + predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI), + traverse_instance_prims=False, ) if len(articulation_prims) != 0: if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get(): diff --git a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py index 363dca41b95..b607f06d088 100644 --- a/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py +++ b/source/isaaclab/isaaclab/assets/rigid_object_collection/rigid_object_collection.py @@ -602,7 +602,9 @@ def _initialize_impl(self): # find rigid root prims root_prims = sim_utils.get_all_matching_child_prims( - template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI) + template_prim_path, + predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI), + traverse_instance_prims=False, ) if len(root_prims) == 0: raise RuntimeError( @@ -618,7 +620,9 @@ def _initialize_impl(self): # check that no rigid object has an articulation root API, which decreases simulation performance articulation_prims = sim_utils.get_all_matching_child_prims( - template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + template_prim_path, + predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI), + traverse_instance_prims=False, ) if len(articulation_prims) != 0: if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get(): diff --git a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py index 1702dbf90e2..dfa816602f8 100644 --- a/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py +++ b/source/isaaclab/isaaclab/assets/surface_gripper/surface_gripper.py @@ -272,7 +272,9 @@ def _initialize_impl(self) -> None: # find surface gripper prims gripper_prims = sim_utils.get_all_matching_child_prims( - template_prim_path, predicate=lambda prim: prim.GetTypeName() == "IsaacSurfaceGripper" + template_prim_path, + predicate=lambda prim: prim.GetTypeName() == "IsaacSurfaceGripper", + traverse_instance_prims=False, ) if len(gripper_prims) == 0: raise RuntimeError( diff --git a/source/isaaclab/isaaclab/sim/utils.py b/source/isaaclab/isaaclab/sim/utils.py index debda3ec807..3d4b8fddb75 100644 --- a/source/isaaclab/isaaclab/sim/utils.py +++ b/source/isaaclab/isaaclab/sim/utils.py @@ -568,7 +568,7 @@ def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = Non # make the prim uninstanceable child_prim.SetInstanceable(False) # add children to list - all_prims += child_prim.GetChildren() + all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) """ @@ -577,14 +577,32 @@ def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = Non def get_first_matching_child_prim( - prim_path: str | Sdf.Path, predicate: Callable[[Usd.Prim], bool], stage: Usd.Stage | None = None + prim_path: str | Sdf.Path, + predicate: Callable[[Usd.Prim], bool], + stage: Usd.Stage | None = None, + traverse_instance_prims: bool = True, ) -> Usd.Prim | None: - """Recursively get the first USD Prim at the path string that passes the predicate function + """Recursively get the first USD Prim at the path string that passes the predicate function. + + This function performs a depth-first traversal of the prim hierarchy starting from + :attr:`prim_path`, returning the first prim that satisfies the provided :attr:`predicate`. + It optionally supports traversal through instance prims, which are normally skipped in standard USD + traversals. + + USD instance prims are lightweight copies of prototype scene structures and are not included + in default traversals unless explicitly handled. This function allows traversing into instances + when :attr:`traverse_instance_prims` is set to :attr:`True`. + + .. versionchanged:: 2.3.0 + + Added :attr:`traverse_instance_prims` to control whether to traverse instance prims. + By default, instance prims are now traversed. Args: prim_path: The path of the prim in the stage. predicate: The function to test the prims against. It takes a prim as input and returns a boolean. stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + traverse_instance_prims: Whether to traverse instance prims. Defaults to True. Returns: The first prim on the path that passes the predicate. If no prim passes the predicate, it returns None. @@ -615,7 +633,10 @@ def get_first_matching_child_prim( if predicate(child_prim): return child_prim # add children to list - all_prims += child_prim.GetChildren() + if traverse_instance_prims: + all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) + else: + all_prims += child_prim.GetChildren() return None @@ -624,9 +645,23 @@ def get_all_matching_child_prims( predicate: Callable[[Usd.Prim], bool] = lambda _: True, depth: int | None = None, stage: Usd.Stage | None = None, + traverse_instance_prims: bool = True, ) -> list[Usd.Prim]: """Performs a search starting from the root and returns all the prims matching the predicate. + This function performs a depth-first traversal of the prim hierarchy starting from + :attr:`prim_path`, returning all prims that satisfy the provided :attr:`predicate`. It optionally + supports traversal through instance prims, which are normally skipped in standard USD traversals. + + USD instance prims are lightweight copies of prototype scene structures and are not included + in default traversals unless explicitly handled. This function allows traversing into instances + when :attr:`traverse_instance_prims` is set to :attr:`True`. + + .. versionchanged:: 2.3.0 + + Added :attr:`traverse_instance_prims` to control whether to traverse instance prims. + By default, instance prims are now traversed. + Args: prim_path: The root prim path to start the search from. predicate: The predicate that checks if the prim matches the desired criteria. It takes a prim as input @@ -634,6 +669,7 @@ def get_all_matching_child_prims( depth: The maximum depth for traversal, should be bigger than zero if specified. Defaults to None (i.e: traversal happens till the end of the tree). stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + traverse_instance_prims: Whether to traverse instance prims. Defaults to True. Returns: A list containing all the prims matching the predicate. @@ -671,7 +707,13 @@ def get_all_matching_child_prims( output_prims.append(child_prim) # add children to list if depth is None or current_depth < depth: - all_prims_queue += [(child, current_depth + 1) for child in child_prim.GetChildren()] + # resolve prims under the current prim + if traverse_instance_prims: + children = child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) + else: + children = child_prim.GetChildren() + # add children to list + all_prims_queue += [(child, current_depth + 1) for child in children] return output_prims diff --git a/source/isaaclab/test/sim/test_utils.py b/source/isaaclab/test/sim/test_utils.py index e4f23438622..62188d8a73a 100644 --- a/source/isaaclab/test/sim/test_utils.py +++ b/source/isaaclab/test/sim/test_utils.py @@ -17,7 +17,7 @@ import isaacsim.core.utils.prims as prim_utils import isaacsim.core.utils.stage as stage_utils import pytest -from pxr import Sdf, Usd, UsdGeom +from pxr import Sdf, Usd, UsdGeom, UsdPhysics import isaaclab.sim as sim_utils from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR @@ -41,21 +41,67 @@ def test_get_all_matching_child_prims(): """Test get_all_matching_child_prims() function.""" # create scene prim_utils.create_prim("/World/Floor") - prim_utils.create_prim( - "/World/Floor/thefloor", "Cube", position=np.array([75, 75, -150.1]), attributes={"size": 300} - ) - prim_utils.create_prim("/World/Room", "Sphere", attributes={"radius": 1e3}) + prim_utils.create_prim("/World/Floor/Box", "Cube", position=np.array([75, 75, -150.1]), attributes={"size": 300}) + prim_utils.create_prim("/World/Wall", "Sphere", attributes={"radius": 1e3}) # test isaac_sim_result = prim_utils.get_all_matching_child_prims("/World") isaaclab_result = sim_utils.get_all_matching_child_prims("/World") assert isaac_sim_result == isaaclab_result + # add articulation root prim -- this asset has instanced prims + # note: isaac sim function does not support instanced prims so we add it here + # after the above test for the above test to still pass. + prim_utils.create_prim( + "/World/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + + # test with predicate + isaaclab_result = sim_utils.get_all_matching_child_prims("/World", predicate=lambda x: x.GetTypeName() == "Cube") + assert len(isaaclab_result) == 1 + assert isaaclab_result[0].GetPrimPath() == "/World/Floor/Box" + + # test with predicate and instanced prims + isaaclab_result = sim_utils.get_all_matching_child_prims( + "/World/Franka/panda_hand/visuals", predicate=lambda x: x.GetTypeName() == "Mesh" + ) + assert len(isaaclab_result) == 1 + assert isaaclab_result[0].GetPrimPath() == "/World/Franka/panda_hand/visuals/panda_hand" + # test valid path with pytest.raises(ValueError): sim_utils.get_all_matching_child_prims("World/Room") +def test_get_first_matching_child_prim(): + """Test get_first_matching_child_prim() function.""" + # create scene + prim_utils.create_prim("/World/Floor") + prim_utils.create_prim( + "/World/env_1/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + prim_utils.create_prim( + "/World/env_2/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + prim_utils.create_prim( + "/World/env_0/Franka", "Xform", usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd" + ) + + # test + isaaclab_result = sim_utils.get_first_matching_child_prim( + "/World", predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + ) + assert isaaclab_result is not None + assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka" + + # test with instanced prims + isaaclab_result = sim_utils.get_first_matching_child_prim( + "/World/env_1/Franka", predicate=lambda prim: prim.GetTypeName() == "Mesh" + ) + assert isaaclab_result is not None + assert isaaclab_result.GetPrimPath() == "/World/env_1/Franka/panda_link0/visuals/panda_link0" + + def test_find_matching_prim_paths(): """Test find_matching_prim_paths() function.""" # create scene From 40554e23e1dc9e7de8e3507e4db076feed8a6ca9 Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Wed, 10 Sep 2025 17:46:43 -0700 Subject: [PATCH 20/50] Adds logdir configuration parameter to environments (#3391) # Description Adds a logdir parameter for all environment configs to allow passing of the log dir for each experiment from the training/inferencing scripts to the environment object. This allows environments to access the logdir for the run and store log files in there, such as from the feature extractor. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Kelly Guo Signed-off-by: Kelly Guo Co-authored-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- scripts/reinforcement_learning/rl_games/play.py | 3 +++ scripts/reinforcement_learning/rl_games/train.py | 3 +++ scripts/reinforcement_learning/rsl_rl/play.py | 3 +++ scripts/reinforcement_learning/rsl_rl/train.py | 3 +++ scripts/reinforcement_learning/sb3/play.py | 3 +++ scripts/reinforcement_learning/sb3/train.py | 3 +++ scripts/reinforcement_learning/skrl/play.py | 3 +++ scripts/reinforcement_learning/skrl/train.py | 3 +++ source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py | 3 +++ source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py | 3 +++ .../isaaclab/isaaclab/envs/manager_based_env_cfg.py | 3 +++ .../direct/shadow_hand/feature_extractor.py | 12 ++++++++---- .../direct/shadow_hand/shadow_hand_vision_env.py | 3 ++- 13 files changed, 43 insertions(+), 5 deletions(-) diff --git a/scripts/reinforcement_learning/rl_games/play.py b/scripts/reinforcement_learning/rl_games/play.py index dd2185b82b0..135980e92c7 100644 --- a/scripts/reinforcement_learning/rl_games/play.py +++ b/scripts/reinforcement_learning/rl_games/play.py @@ -130,6 +130,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen resume_path = retrieve_file_path(args_cli.checkpoint) log_dir = os.path.dirname(os.path.dirname(resume_path)) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # wrap around environment for rl-games rl_device = agent_cfg["params"]["config"]["device"] clip_obs = agent_cfg["params"]["env"].get("clip_observations", math.inf) diff --git a/scripts/reinforcement_learning/rl_games/train.py b/scripts/reinforcement_learning/rl_games/train.py index 711682f83e4..59fb9144a4d 100644 --- a/scripts/reinforcement_learning/rl_games/train.py +++ b/scripts/reinforcement_learning/rl_games/train.py @@ -165,6 +165,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/reinforcement_learning/rsl_rl/play.py b/scripts/reinforcement_learning/rsl_rl/play.py index 9e89c6ff318..11ef7399462 100644 --- a/scripts/reinforcement_learning/rsl_rl/play.py +++ b/scripts/reinforcement_learning/rsl_rl/play.py @@ -112,6 +112,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen log_dir = os.path.dirname(resume_path) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/reinforcement_learning/rsl_rl/train.py b/scripts/reinforcement_learning/rsl_rl/train.py index 33bfc9f63d4..ea630c5988d 100644 --- a/scripts/reinforcement_learning/rsl_rl/train.py +++ b/scripts/reinforcement_learning/rsl_rl/train.py @@ -150,6 +150,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/reinforcement_learning/sb3/play.py b/scripts/reinforcement_learning/sb3/play.py index 05c52390749..c803c1807ba 100644 --- a/scripts/reinforcement_learning/sb3/play.py +++ b/scripts/reinforcement_learning/sb3/play.py @@ -127,6 +127,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen checkpoint_path = args_cli.checkpoint log_dir = os.path.dirname(checkpoint_path) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/reinforcement_learning/sb3/train.py b/scripts/reinforcement_learning/sb3/train.py index ba45398f108..b2a9832a117 100644 --- a/scripts/reinforcement_learning/sb3/train.py +++ b/scripts/reinforcement_learning/sb3/train.py @@ -152,6 +152,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/reinforcement_learning/skrl/play.py b/scripts/reinforcement_learning/skrl/play.py index 990ef5b558d..b4d52c39e8c 100644 --- a/scripts/reinforcement_learning/skrl/play.py +++ b/scripts/reinforcement_learning/skrl/play.py @@ -165,6 +165,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, expe ) log_dir = os.path.dirname(os.path.dirname(resume_path)) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/scripts/reinforcement_learning/skrl/train.py b/scripts/reinforcement_learning/skrl/train.py index e3399f204b5..c66efae0138 100644 --- a/scripts/reinforcement_learning/skrl/train.py +++ b/scripts/reinforcement_learning/skrl/train.py @@ -182,6 +182,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) + # set the log directory for the environment (works for all environment types) + env_cfg.log_dir = log_dir + # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) diff --git a/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py b/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py index 210b5139730..15f57cb4c03 100644 --- a/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/direct_marl_env_cfg.py @@ -225,3 +225,6 @@ class DirectMARLEnvCfg: xr: XrCfg | None = None """Configuration for viewing and interacting with the environment through an XR device.""" + + log_dir: str | None = None + """Directory for logging experiment artifacts. Defaults to None, in which case no specific log directory is set.""" diff --git a/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py b/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py index 6b26bdc7500..33297a228af 100644 --- a/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/direct_rl_env_cfg.py @@ -229,3 +229,6 @@ class DirectRLEnvCfg: xr: XrCfg | None = None """Configuration for viewing and interacting with the environment through an XR device.""" + + log_dir: str | None = None + """Directory for logging experiment artifacts. Defaults to None, in which case no specific log directory is set.""" diff --git a/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py b/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py index a50e5336f9b..1a6bdacb795 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py @@ -131,3 +131,6 @@ class ManagerBasedEnvCfg: io_descriptors_output_dir: str | None = None """The directory to export the IO descriptors to. Defaults to None.""" + + log_dir: str | None = None + """Directory for logging experiment artifacts. Defaults to None, in which case no specific log directory is set.""" diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py index 60a27649119..82d76ec7f1e 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/feature_extractor.py @@ -73,12 +73,13 @@ class FeatureExtractor: If the train flag is set to True, the CNN is trained during the rollout process. """ - def __init__(self, cfg: FeatureExtractorCfg, device: str): + def __init__(self, cfg: FeatureExtractorCfg, device: str, log_dir: str | None = None): """Initialize the feature extractor model. Args: - cfg (FeatureExtractorCfg): Configuration for the feature extractor model. - device (str): Device to run the model on. + cfg: Configuration for the feature extractor model. + device: Device to run the model on. + log_dir: Directory to save checkpoints. If None, uses local "logs" folder resolved with respect to this file. """ self.cfg = cfg @@ -89,7 +90,10 @@ def __init__(self, cfg: FeatureExtractorCfg, device: str): self.feature_extractor.to(self.device) self.step_count = 0 - self.log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "logs") + if log_dir is not None: + self.log_dir = log_dir + else: + self.log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "logs") if not os.path.exists(self.log_dir): os.makedirs(self.log_dir) diff --git a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py index 6cde7d06fc1..42e8c4f03c4 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/direct/shadow_hand/shadow_hand_vision_env.py @@ -65,7 +65,8 @@ class ShadowHandVisionEnv(InHandManipulationEnv): def __init__(self, cfg: ShadowHandVisionEnvCfg, render_mode: str | None = None, **kwargs): super().__init__(cfg, render_mode, **kwargs) - self.feature_extractor = FeatureExtractor(self.cfg.feature_extractor, self.device) + # Use the log directory from the configuration + self.feature_extractor = FeatureExtractor(self.cfg.feature_extractor, self.device, self.cfg.log_dir) # hide goal cubes self.goal_pos[:, :] = torch.tensor([-0.2, 0.1, 0.6], device=self.device) # keypoints buffer From 8704741e605ac24d803229fd608f8e26dd12e187 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:35:31 +0200 Subject: [PATCH 21/50] Adds Github workflow for labelling PRs (#3404) # Description Given the increase in number of PRs to the codebase, it has become harder to understand the type of changes and the corresponding team to communicate with. This MR uses a GitHub bot to help us out with categorizing the PRs. More info: https://github.com/actions/labeler ## Type of change - New feature (non-breaking change which adds functionality) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- .github/labeler.yml | 73 +++++++++++++++++++++++++++++++++++ .github/workflows/labeler.yml | 17 ++++++++ 2 files changed, 90 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000000..c3fde088a95 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,73 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +# Documentation-related changes +documentation: + - changed-files: + - any-glob-to-any-file: + - '**/docs/**' + - '**/README.md' + - all-globs-to-all-files: + - '!**/CHANGELOG.rst' + +# Infrastructure changes +infrastructure: + - changed-files: + - any-glob-to-any-file: + - .github/** + - docker/** + - .dockerignore + - tools/** + - .vscode/** + - environment.yml + - setup.py + - pyproject.toml + - .pre-commit-config.yaml + - .flake8 + - isaaclab.sh + - isaaclab.bat + +# Assets (USD, glTF, etc.) related changes. +asset: + - changed-files: + - any-glob-to-any-file: + - source/isaaclab_assets/** + +# Isaac Sim team related changes. +isaac-sim: + - changed-files: + - any-glob-to-any-file: + - apps/** + +# Isaac Mimic team related changes. +isaac-mimic: + - changed-files: + - any-glob-to-any-file: + - source/isaaclab/isaaclab/devices/** + - source/isaaclab_mimic/** + - source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack** + - source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/pick_and_place** + - scripts/imitation_learning/** + +# Isaac Lab team related changes. +isaac-lab: + - changed-files: + - any-glob-to-any-file: + - source/** + - scripts/** + - all-globs-to-all-files: + - '!source/isaaclab_assets/**' + - '!source/isaaclab_mimic/**' + - '!scripts/imitation_learning/**' + +# Add 'enhancement' label to any PR where the head branch name +# starts with `feature` or has a `feature` section in the name +enhancement: + - head-branch: ['^feature', 'feature'] + +# Add 'bug' label to any PR where the head branch name +# starts with `fix`/`bug` or has a `fix`/`bug` section in the name +bug: + - head-branch: ['^fix', 'fix', '^bug', 'bug'] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000000..8b06dc14407 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,17 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 From 7c2a7af73ce8f16ae30a33594f7f4bdb3a46e3d4 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:49:02 +0200 Subject: [PATCH 22/50] Adds license for GitHub labeler dependency (#3435) # Description This MR adds license for Github action labeller and Pinocchio. ## Type of change - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- .github/labeler.yml | 4 ++- ...nschema-license => jsonschema-license.txt} | 0 ... => jsonschema-specifications-license.txt} | 0 .../licenses/dependencies/labeler-license.txt | 21 +++++++++++++++ .../dependencies/pinocchio-license.txt | 26 +++++++++++++++++++ .../{pygame-license => pygame-license.txt} | 0 ...encing-license => referencing-license.txt} | 0 ...-license => typing-inspection-license.txt} | 0 .../{zipp-license => zipp-license.txt} | 0 9 files changed, 50 insertions(+), 1 deletion(-) rename docs/licenses/dependencies/{jsonschema-license => jsonschema-license.txt} (100%) rename docs/licenses/dependencies/{jsonschema-specifications-license => jsonschema-specifications-license.txt} (100%) create mode 100644 docs/licenses/dependencies/labeler-license.txt create mode 100644 docs/licenses/dependencies/pinocchio-license.txt rename docs/licenses/dependencies/{pygame-license => pygame-license.txt} (100%) rename docs/licenses/dependencies/{referencing-license => referencing-license.txt} (100%) rename docs/licenses/dependencies/{typing-inspection-license => typing-inspection-license.txt} (100%) rename docs/licenses/dependencies/{zipp-license => zipp-license.txt} (100%) diff --git a/.github/labeler.yml b/.github/labeler.yml index c3fde088a95..b4035375319 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -7,10 +7,11 @@ documentation: - changed-files: - any-glob-to-any-file: - - '**/docs/**' + - 'docs/**' - '**/README.md' - all-globs-to-all-files: - '!**/CHANGELOG.rst' + - '!docs/licenses/**' # Infrastructure changes infrastructure: @@ -28,6 +29,7 @@ infrastructure: - .flake8 - isaaclab.sh - isaaclab.bat + - docs/licenses/** # Assets (USD, glTF, etc.) related changes. asset: diff --git a/docs/licenses/dependencies/jsonschema-license b/docs/licenses/dependencies/jsonschema-license.txt similarity index 100% rename from docs/licenses/dependencies/jsonschema-license rename to docs/licenses/dependencies/jsonschema-license.txt diff --git a/docs/licenses/dependencies/jsonschema-specifications-license b/docs/licenses/dependencies/jsonschema-specifications-license.txt similarity index 100% rename from docs/licenses/dependencies/jsonschema-specifications-license rename to docs/licenses/dependencies/jsonschema-specifications-license.txt diff --git a/docs/licenses/dependencies/labeler-license.txt b/docs/licenses/dependencies/labeler-license.txt new file mode 100644 index 00000000000..cfbc8bb6dda --- /dev/null +++ b/docs/licenses/dependencies/labeler-license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 GitHub, Inc. and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/licenses/dependencies/pinocchio-license.txt b/docs/licenses/dependencies/pinocchio-license.txt new file mode 100644 index 00000000000..dfacb673148 --- /dev/null +++ b/docs/licenses/dependencies/pinocchio-license.txt @@ -0,0 +1,26 @@ +BSD 2-Clause License + +Copyright (c) 2014-2023, CNRS +Copyright (c) 2018-2025, INRIA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/licenses/dependencies/pygame-license b/docs/licenses/dependencies/pygame-license.txt similarity index 100% rename from docs/licenses/dependencies/pygame-license rename to docs/licenses/dependencies/pygame-license.txt diff --git a/docs/licenses/dependencies/referencing-license b/docs/licenses/dependencies/referencing-license.txt similarity index 100% rename from docs/licenses/dependencies/referencing-license rename to docs/licenses/dependencies/referencing-license.txt diff --git a/docs/licenses/dependencies/typing-inspection-license b/docs/licenses/dependencies/typing-inspection-license.txt similarity index 100% rename from docs/licenses/dependencies/typing-inspection-license rename to docs/licenses/dependencies/typing-inspection-license.txt diff --git a/docs/licenses/dependencies/zipp-license b/docs/licenses/dependencies/zipp-license.txt similarity index 100% rename from docs/licenses/dependencies/zipp-license rename to docs/licenses/dependencies/zipp-license.txt From e3f43d9ea3a7375e54e0f8253897d318ee8d0367 Mon Sep 17 00:00:00 2001 From: Cathy Li <40371641+cathyliyuanchen@users.noreply.github.com> Date: Thu, 11 Sep 2025 00:52:18 -0700 Subject: [PATCH 23/50] Fixes terminal output in Manus OpenXR device (#3430) # Description Update the user on wrist calibration status with terminal outputs. Remove unnecessary error logs at the beginning of the session, when joint data has not arrived yet. ## Type of change - Bug fix (non-breaking change which fixes an issue) --------- Co-authored-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- .../devices/openxr/manus_vive_utils.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py b/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py index c58e32fa0d2..12ca81ffaae 100644 --- a/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py +++ b/source/isaaclab/isaaclab/devices/openxr/manus_vive_utils.py @@ -7,7 +7,7 @@ import numpy as np from time import time -import carb +import omni.log from isaacsim.core.utils.extensions import enable_extension # For testing purposes, we need to mock the XRCore @@ -144,7 +144,7 @@ def update_vive(self): if self.scene_T_lighthouse_static is None: self._initialize_coordinate_transformation() except Exception as e: - carb.log_error(f"Vive tracker update failed: {e}") + omni.log.error(f"Vive tracker update failed: {e}") def _initialize_coordinate_transformation(self): """ @@ -214,8 +214,12 @@ def _initialize_coordinate_transformation(self): choose_A = True elif errB < errA and errB < tolerance: choose_A = False + elif len(self._pairA_trans_errs) % 10 == 0 or len(self._pairB_trans_errs) % 10 == 0: + print("Computing pairing of Vive trackers with wrists") + omni.log.info( + f"Pairing Vive trackers with wrists: error of pairing A: {errA}, error of pairing B: {errB}" + ) if choose_A is None: - carb.log_info(f"error A: {errA}, error B: {errB}") return if choose_A: @@ -227,14 +231,21 @@ def _initialize_coordinate_transformation(self): if len(chosen_list) >= min_frames: cluster = select_mode_cluster(chosen_list) - carb.log_info(f"Wrist calibration: formed size {len(cluster)} cluster from {len(chosen_list)} samples") + if len(chosen_list) % 10 == 0: + print( + f"Computing wrist calibration: formed size {len(cluster)} cluster from" + f" {len(chosen_list)} samples" + ) if len(cluster) >= min_frames // 2: averaged = average_transforms(cluster) self.scene_T_lighthouse_static = averaged - carb.log_info(f"Resolved mapping: {self._vive_left_id}->Left, {self._vive_right_id}->Right") + print( + f"Wrist calibration computed. Resolved mapping: {self._vive_left_id}->Left," + f" {self._vive_right_id}->Right" + ) except Exception as e: - carb.log_error(f"Failed to initialize coordinate transformation: {e}") + omni.log.error(f"Failed to initialize coordinate transformation: {e}") def _transform_vive_data(self, device_data: dict) -> dict: """Transform Vive tracker poses to scene coordinates. @@ -304,7 +315,7 @@ def _get_palm(self, transformed_data: dict, hand: str) -> dict: Pose dictionary with 'position' and 'orientation'. """ if f"{hand}_6" not in transformed_data or f"{hand}_7" not in transformed_data: - carb.log_error(f"Joint data not found for {hand}") + # Joint data not arrived yet return self.default_pose metacarpal = transformed_data[f"{hand}_6"] proximal = transformed_data[f"{hand}_7"] @@ -422,7 +433,7 @@ def get_openxr_wrist_matrix(hand: str) -> Gf.Matrix4d: return None return joint.pose_matrix except Exception as e: - carb.log_warn(f"OpenXR {hand} wrist fetch failed: {e}") + omni.log.warn(f"OpenXR {hand} wrist fetch failed: {e}") return None From b7a46b527655d32cea2087570a01e5ab76d043a0 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:20:43 +0200 Subject: [PATCH 24/50] Adds fine-grained control to GitHub action labeler (#3436) Previous merge led to matches for documentation that are unintentional. This MR fixes the workflow. - Bug fix (non-breaking change which fixes an issue) - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- .github/labeler.yml | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index b4035375319..8c9d116aefc 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -5,13 +5,13 @@ # Documentation-related changes documentation: - - changed-files: - - any-glob-to-any-file: - - 'docs/**' - - '**/README.md' - - all-globs-to-all-files: - - '!**/CHANGELOG.rst' - - '!docs/licenses/**' + - all: + - changed-files: + - any-glob-to-any-file: + - 'docs/**' + - '**/README.md' + - all-globs-to-all-files: + - '!docs/licenses/**' # Infrastructure changes infrastructure: @@ -55,14 +55,16 @@ isaac-mimic: # Isaac Lab team related changes. isaac-lab: - - changed-files: - - any-glob-to-any-file: - - source/** - - scripts/** - - all-globs-to-all-files: - - '!source/isaaclab_assets/**' - - '!source/isaaclab_mimic/**' - - '!scripts/imitation_learning/**' + - all: + - changed-files: + - any-glob-to-any-file: + - source/** + - scripts/** + - all-globs-to-all-files: + - '!source/isaaclab_assets/**' + - '!source/isaaclab_mimic/**' + - '!source/isaaclab/isaaclab/devices' + - '!scripts/imitation_learning/**' # Add 'enhancement' label to any PR where the head branch name # starts with `feature` or has a `feature` section in the name From 649ad88bfcd097dbbd4eee14d6c9ece361ed7f39 Mon Sep 17 00:00:00 2001 From: Louis LE LAY Date: Thu, 11 Sep 2025 04:34:21 -0400 Subject: [PATCH 25/50] Fixes tensor construction warning in `events.py` (#3251) # Description This PR removes a `UserWarning` from PyTorch about using `torch.tensor()` on an existing tensor in `events.py`. It replaces `torch.tensor(actuator.joint_indices, device=asset.device)` with `.to(device)` to avoid unnecessary copies. Warning mentionned: ```bash /home/spring/IsaacLab/source/isaaclab/isaaclab/envs/mdp/events.py:542: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor). actuator_joint_indices = torch.tensor(actuator.joint_indices, device=asset.device) ``` ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Louis LE LAY Co-authored-by: ooctipus Co-authored-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- source/isaaclab/isaaclab/envs/mdp/events.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py index 17c5f582d1e..5a6ec632d08 100644 --- a/source/isaaclab/isaaclab/envs/mdp/events.py +++ b/source/isaaclab/isaaclab/envs/mdp/events.py @@ -596,14 +596,16 @@ def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: actuator_indices = slice(None) if isinstance(actuator.joint_indices, slice): global_indices = slice(None) + elif isinstance(actuator.joint_indices, torch.Tensor): + global_indices = actuator.joint_indices.to(self.asset.device) else: - global_indices = torch.tensor(actuator.joint_indices, device=self.asset.device) + raise TypeError("Actuator joint indices must be a slice or a torch.Tensor.") elif isinstance(actuator.joint_indices, slice): # we take the joints defined in the asset config - global_indices = actuator_indices = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) + global_indices = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) else: # we take the intersection of the actuator joints and the asset config joints - actuator_joint_indices = torch.tensor(actuator.joint_indices, device=self.asset.device) + actuator_joint_indices = actuator.joint_indices asset_joint_ids = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) # the indices of the joints in the actuator that have to be randomized actuator_indices = torch.nonzero(torch.isin(actuator_joint_indices, asset_joint_ids)).view(-1) From e4924f2264b440b400fd82eb2d93ad54ecff4c2e Mon Sep 17 00:00:00 2001 From: Javier Felix-Rendon Date: Thu, 11 Sep 2025 02:51:15 -0600 Subject: [PATCH 26/50] Fixes jetbot asset path in technical_env_design.rst (#3328) Fixes Jetbot asset path in the documentation. - Documentation update - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there Signed-off-by: Javier Felix-Rendon --- .github/labeler.yml | 2 +- docs/source/setup/walkthrough/technical_env_design.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 8c9d116aefc..c6869bb3c4c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -11,7 +11,7 @@ documentation: - 'docs/**' - '**/README.md' - all-globs-to-all-files: - - '!docs/licenses/**' + - '!docs/licenses/**' # Infrastructure changes infrastructure: diff --git a/docs/source/setup/walkthrough/technical_env_design.rst b/docs/source/setup/walkthrough/technical_env_design.rst index f1774a2804a..982a579f683 100644 --- a/docs/source/setup/walkthrough/technical_env_design.rst +++ b/docs/source/setup/walkthrough/technical_env_design.rst @@ -35,7 +35,7 @@ The contents of ``jetbot.py`` is fairly minimal from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR JETBOT_CONFIG = ArticulationCfg( - spawn=sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/Jetbot/jetbot.usd"), + spawn=sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/NVIDIA/Jetbot/jetbot.usd"), actuators={"wheel_acts": ImplicitActuatorCfg(joint_names_expr=[".*"], damping=None, stiffness=None)}, ) From c007680e2ec1a2e1be43357269056d3677b23dc7 Mon Sep 17 00:00:00 2001 From: Louis LE LAY Date: Thu, 11 Sep 2025 05:05:24 -0400 Subject: [PATCH 27/50] Restricts .gitignore rule to top-level datasets/ directory (#3400) # Description As noted by @liyifan2002 in the related issue, the `.gitignore` rule for `datasets` also unintentionally ignores changes in `isaaclab/utils/datasets/`. This PR fixes the problem by changing the rule from `datasets` to `/datasets/`, ensuring that only the top-level `datasets/` folder (e.g., created when following [this guide](https://isaac-sim.github.io/IsaacLab/main/source/overview/imitation-learning/skillgen.html#download-and-setup)) is ignored, while keeping `isaaclab/utils/datasets/` tracked. Fixes https://github.com/isaac-sim/IsaacLab/issues/3364 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b6c57b6313c..08d2e8dee5a 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,7 @@ _build /.pretrained_checkpoints/ # Teleop Recorded Dataset -datasets +/datasets/ # Tests tests/ From 9e327f2696c07b551b3991cb22558010a4a1102f Mon Sep 17 00:00:00 2001 From: Doug Fulop Date: Thu, 11 Sep 2025 09:13:20 -0700 Subject: [PATCH 28/50] Fixes symbol in training_jetbot_reward_exploration.rst (#2722) # Description Just fixed a doc formatting issue (an extra space prevents proper syntax highlighting on this documentation page). ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there Signed-off-by: Doug Fulop From d5d571121c7e1f023b496da7a2fe6dd6187f210b Mon Sep 17 00:00:00 2001 From: Kyle Morgenstein <34984693+KyleM73@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:14:36 -0500 Subject: [PATCH 29/50] Adds uv support as an alternative to conda in isaaclab.sh (#3172) # Description This PR adds support for uv as an alternative to conda or venv for managing virtual environments and adds corresponding support for uv pip for managing python dependencies. uv and uv pip is significantly faster than conda and has many useful tools. If users wish to use the uv workflow they will need to have uv installed, but otherwise no additional dependencies are added. Docs should be updated to describe this option. There may need to be more work done to clean when deactivating the environment. Uv does not support pre and post activation hooks like conda so cleaning up the environment variables is slightly more tricky. I would greatly appreciate feedback to improve this workflow! Fixes #3408 ## Type of change - New feature (non-breaking change which adds functionality) - This change requires a documentation update ## Checklist - [X] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [X] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [X] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Kyle Morgenstein <34984693+KyleM73@users.noreply.github.com> Signed-off-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Co-authored-by: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> --- docs/licenses/dependencies/uv-license.txt | 21 +++ .../overview/developer-guide/vs_code.rst | 2 +- docs/source/overview/own-project/template.rst | 2 +- .../installation/binaries_installation.rst | 57 ++++++- .../isaaclab_pip_installation.rst | 29 ++++ .../setup/installation/pip_installation.rst | 30 ++++ docs/source/setup/quickstart.rst | 38 ++++- isaaclab.sh | 158 +++++++++++++++--- tools/template/templates/external/README.md | 2 +- 9 files changed, 310 insertions(+), 29 deletions(-) create mode 100644 docs/licenses/dependencies/uv-license.txt diff --git a/docs/licenses/dependencies/uv-license.txt b/docs/licenses/dependencies/uv-license.txt new file mode 100644 index 00000000000..01483514487 --- /dev/null +++ b/docs/licenses/dependencies/uv-license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Astral Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/source/overview/developer-guide/vs_code.rst b/docs/source/overview/developer-guide/vs_code.rst index f9ea07b6da3..6e69ab407d4 100644 --- a/docs/source/overview/developer-guide/vs_code.rst +++ b/docs/source/overview/developer-guide/vs_code.rst @@ -69,7 +69,7 @@ python executable provided by Omniverse. This is specified in the "python.defaultInterpreterPath": "${workspaceFolder}/_isaac_sim/python.sh", } -If you want to use a different python interpreter (for instance, from your conda environment), +If you want to use a different python interpreter (for instance, from your conda or uv environment), you need to change the python interpreter used by selecting and activating the python interpreter of your choice in the bottom left corner of VSCode, or opening the command palette (``Ctrl+Shift+P``) and selecting ``Python: Select Interpreter``. diff --git a/docs/source/overview/own-project/template.rst b/docs/source/overview/own-project/template.rst index 521b959a748..cb52effde62 100644 --- a/docs/source/overview/own-project/template.rst +++ b/docs/source/overview/own-project/template.rst @@ -33,7 +33,7 @@ Running the template generator ------------------------------ Install Isaac Lab by following the `installation guide <../../setup/installation/index.html>`_. -We recommend using conda installation as it simplifies calling Python scripts from the terminal. +We recommend using conda or uv installation as it simplifies calling Python scripts from the terminal. Then, run the following command to generate a new external project or internal task: diff --git a/docs/source/setup/installation/binaries_installation.rst b/docs/source/setup/installation/binaries_installation.rst index d066f7ce0b0..2ffddda6ece 100644 --- a/docs/source/setup/installation/binaries_installation.rst +++ b/docs/source/setup/installation/binaries_installation.rst @@ -213,6 +213,7 @@ Clone the Isaac Lab repository into your workspace: -d, --docs Build the documentation from source using sphinx. -n, --new Create a new external project or internal task from template. -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows @@ -234,6 +235,7 @@ Clone the Isaac Lab repository into your workspace: -d, --docs Build the documentation from source using sphinx. -n, --new Create a new external project or internal task from template. -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. Creating the Isaac Sim Symbolic Link @@ -268,6 +270,57 @@ to index the python modules and look for extensions shipped with Isaac Sim. mklink /D _isaac_sim path_to_isaac_sim :: For example: mklink /D _isaac_sim C:/isaacsim +Setting up the uv environment (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attention:: + This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. + +The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac +Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable +behaves like a python executable, and can be used to run any python script or +module with the simulator. For more information, please refer to the +`documentation `__. + +To install ``uv``, please follow the instructions `here `__. +You can create the Isaac Lab environment using the following commands. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default name for uv environment is 'env_isaaclab' + ./isaaclab.sh --uv # or "./isaaclab.sh -u" + # Option 2: Custom name for uv environment + ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Default name for uv environment is 'env_isaaclab' + isaaclab.bat --uv :: or "isaaclab.bat -u" + :: Option 2: Custom name for uv environment + isaaclab.bat --uv my_env :: or "isaaclab.bat -u my_env" + + +Once created, be sure to activate the environment before proceeding! + +.. code:: bash + + source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" + +Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` +to run python scripts. You can use the default python executable in your environment +by running ``python`` or ``python3``. However, for the rest of the documentation, +we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command +is equivalent to running ``python`` or ``python3`` in your virtual environment. + Setting up the conda environment (optional) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -427,8 +480,8 @@ On Windows machines, please terminate the process from Command Prompt using If you see this, then the installation was successful! |:tada:| -If you see an error ``ModuleNotFoundError: No module named 'isaacsim'``, ensure that the conda environment is activated -and ``source _isaac_sim/setup_conda_env.sh`` has been executed. +If you see an error ``ModuleNotFoundError: No module named 'isaacsim'``, ensure that the conda or uv environment is activated +and ``source _isaac_sim/setup_conda_env.sh`` has been executed (for uv as well). Train a robot! diff --git a/docs/source/setup/installation/isaaclab_pip_installation.rst b/docs/source/setup/installation/isaaclab_pip_installation.rst index 2267e0cc5ec..153d575fd99 100644 --- a/docs/source/setup/installation/isaaclab_pip_installation.rst +++ b/docs/source/setup/installation/isaaclab_pip_installation.rst @@ -25,6 +25,31 @@ To learn about how to set up your own project on top of Isaac Lab, see :ref:`tem conda create -n env_isaaclab python=3.11 conda activate env_isaaclab + .. tab-item:: uv environment + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + source env_isaaclab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + env_isaaclab\Scripts\activate + .. tab-item:: venv environment .. tab-set:: @@ -70,6 +95,10 @@ To learn about how to set up your own project on top of Isaac Lab, see :ref:`tem python -m pip install --upgrade pip +.. note:: + + If you use uv, replace ``pip`` with ``uv pip``. + - Next, install a CUDA-enabled PyTorch 2.7.0 build for CUDA 12.8. diff --git a/docs/source/setup/installation/pip_installation.rst b/docs/source/setup/installation/pip_installation.rst index 48952959e59..73d24b0fef8 100644 --- a/docs/source/setup/installation/pip_installation.rst +++ b/docs/source/setup/installation/pip_installation.rst @@ -49,6 +49,31 @@ If you encounter any issues, please report them to the conda create -n env_isaaclab python=3.11 conda activate env_isaaclab + .. tab-item:: uv environment + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + source env_isaaclab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + env_isaaclab\Scripts\activate + .. tab-item:: venv environment .. tab-set:: @@ -94,6 +119,9 @@ If you encounter any issues, please report them to the python -m pip install --upgrade pip +.. note:: + + If you use uv, replace ``pip`` with ``uv pip``. - Next, install a CUDA-enabled PyTorch 2.7.0 build. @@ -216,6 +244,7 @@ Clone the Isaac Lab repository into your workspace: -d, --docs Build the documentation from source using sphinx. -n, --new Create a new external project or internal task from template. -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows @@ -237,6 +266,7 @@ Clone the Isaac Lab repository into your workspace: -d, --docs Build the documentation from source using sphinx. -n, --new Create a new external project or internal task from template. -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. Installation ~~~~~~~~~~~~ diff --git a/docs/source/setup/quickstart.rst b/docs/source/setup/quickstart.rst index a42bc665570..13e0dbadaab 100644 --- a/docs/source/setup/quickstart.rst +++ b/docs/source/setup/quickstart.rst @@ -29,13 +29,41 @@ pip install route using virtual environments. To begin, we first define our virtual environment. +.. tab-set:: -.. code-block:: bash + .. tab-item:: conda + + .. code-block:: bash + + # create a virtual environment named env_isaaclab with python3.11 + conda create -n env_isaaclab python=3.11 + # activate the virtual environment + conda activate env_isaaclab + + .. tab-item:: uv + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + source env_isaaclab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch - # create a virtual environment named env_isaaclab with python3.11 - conda create -n env_isaaclab python=3.11 - # activate the virtual environment - conda activate env_isaaclab + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + env_isaaclab\Scripts\activate Next, install a CUDA-enabled PyTorch 2.7.0 build. diff --git a/isaaclab.sh b/isaaclab.sh index 3c8cf33a05d..5d5bc684641 100755 --- a/isaaclab.sh +++ b/isaaclab.sh @@ -97,26 +97,27 @@ is_docker() { } ensure_cuda_torch() { - local py="$1" + local pip_command=$(extract_pip_command) + local pip_uninstall_command=$(extract_pip_uninstall_command) local -r TORCH_VER="2.7.0" local -r TV_VER="0.22.0" local -r CUDA_TAG="cu128" local -r PYTORCH_INDEX="https://download.pytorch.org/whl/${CUDA_TAG}" local torch_ver - if "$py" -m pip show torch >/dev/null 2>&1; then - torch_ver="$("$py" -m pip show torch 2>/dev/null | awk -F': ' '/^Version/{print $2}')" + if "$pip_command" show torch >/dev/null 2>&1; then + torch_ver="$("$pip_command" show torch 2>/dev/null | awk -F': ' '/^Version/{print $2}')" echo "[INFO] Found PyTorch version ${torch_ver}." if [[ "$torch_ver" != "${TORCH_VER}+${CUDA_TAG}" ]]; then echo "[INFO] Replacing PyTorch ${torch_ver} → ${TORCH_VER}+${CUDA_TAG}..." - "$py" -m pip uninstall -y torch torchvision torchaudio >/dev/null 2>&1 || true - "$py" -m pip install "torch==${TORCH_VER}" "torchvision==${TV_VER}" --index-url "${PYTORCH_INDEX}" + "$pip_uninstall_command" torch torchvision torchaudio >/dev/null 2>&1 || true + "$pip_command" "torch==${TORCH_VER}" "torchvision==${TV_VER}" --index-url "${PYTORCH_INDEX}" else echo "[INFO] PyTorch ${TORCH_VER}+${CUDA_TAG} already installed." fi else echo "[INFO] Installing PyTorch ${TORCH_VER}+${CUDA_TAG}..." - "$py" -m pip install "torch==${TORCH_VER}" "torchvision==${TV_VER}" --index-url "${PYTORCH_INDEX}" + ${pip_command} "torch==${TORCH_VER}" "torchvision==${TV_VER}" --index-url "${PYTORCH_INDEX}" fi } @@ -154,6 +155,9 @@ extract_python_exe() { if ! [[ -z "${CONDA_PREFIX}" ]]; then # use conda python local python_exe=${CONDA_PREFIX}/bin/python + elif ! [[ -z "${VIRTUAL_ENV}" ]]; then + # use uv virtual environment python + local python_exe=${VIRTUAL_ENV}/bin/python else # use kit python local python_exe=${ISAACLAB_PATH}/_isaac_sim/python.sh @@ -171,7 +175,7 @@ extract_python_exe() { if [ ! -f "${python_exe}" ]; then echo -e "[ERROR] Unable to find any Python executable at path: '${python_exe}'" >&2 echo -e "\tThis could be due to the following reasons:" >&2 - echo -e "\t1. Conda environment is not activated." >&2 + echo -e "\t1. Conda or uv environment is not activated." >&2 echo -e "\t2. Isaac Sim pip package 'isaacsim-rl' is not installed." >&2 echo -e "\t3. Python executable is not available at the default path: ${ISAACLAB_PATH}/_isaac_sim/python.sh" >&2 exit 1 @@ -203,14 +207,43 @@ extract_isaacsim_exe() { echo ${isaacsim_exe} } +# find pip command based on virtualization +extract_pip_command() { + # detect if we're in a uv environment + if [ -n "${VIRTUAL_ENV}" ] && [ -f "${VIRTUAL_ENV}/pyvenv.cfg" ] && grep -q "uv" "${VIRTUAL_ENV}/pyvenv.cfg"; then + pip_command="uv pip install" + else + # retrieve the python executable + python_exe=$(extract_python_exe) + pip_command="${python_exe} -m pip install" + fi + + echo ${pip_command} +} + +extract_pip_uninstall_command() { + # detect if we're in a uv environment + if [ -n "${VIRTUAL_ENV}" ] && [ -f "${VIRTUAL_ENV}/pyvenv.cfg" ] && grep -q "uv" "${VIRTUAL_ENV}/pyvenv.cfg"; then + pip_uninstall_command="uv pip uninstall" + else + # retrieve the python executable + python_exe=$(extract_python_exe) + pip_uninstall_command="${python_exe} -m pip uninstall -y" + fi + + echo ${pip_uninstall_command} +} + # check if input directory is a python extension and install the module install_isaaclab_extension() { # retrieve the python executable python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) + # if the directory contains setup.py then install the python module if [ -f "$1/setup.py" ]; then echo -e "\t module: $1" - ${python_exe} -m pip install --editable $1 + $pip_command --editable "$1" fi } @@ -331,6 +364,68 @@ setup_conda_env() { echo -e "\n" } +# setup uv environment for Isaac Lab +setup_uv_env() { + # get environment name from input + local env_name="$1" + local python_path="$2" + + # check uv is installed + if ! command -v uv &>/dev/null; then + echo "[ERROR] uv could not be found. Please install uv and try again." + echo "[ERROR] uv can be installed here:" + echo "[ERROR] https://docs.astral.sh/uv/getting-started/installation/" + exit 1 + fi + + # check if _isaac_sim symlink exists and isaacsim-rl is not installed via pip + if [ ! -L "${ISAACLAB_PATH}/_isaac_sim" ] && ! python -m pip list | grep -q 'isaacsim-rl'; then + echo -e "[WARNING] _isaac_sim symlink not found at ${ISAACLAB_PATH}/_isaac_sim" + echo -e "\tThis warning can be ignored if you plan to install Isaac Sim via pip." + echo -e "\tIf you are using a binary installation of Isaac Sim, please ensure the symlink is created before setting up the conda environment." + fi + + # check if the environment exists + local env_path="${ISAACLAB_PATH}/${env_name}" + if [ ! -d "${env_path}" ]; then + echo -e "[INFO] Creating uv environment named '${env_name}'..." + uv venv --clear --python "${python_path}" "${env_path}" + else + echo "[INFO] uv environment '${env_name}' already exists." + fi + + # define root path for activation hooks + local isaaclab_root="${ISAACLAB_PATH}" + + # cache current paths for later + cache_pythonpath=$PYTHONPATH + cache_ld_library_path=$LD_LIBRARY_PATH + + # ensure activate file exists + touch "${env_path}/bin/activate" + + # add variables to environment during activation + cat >> "${env_path}/bin/activate" <&2 } @@ -386,12 +482,17 @@ while [[ $# -gt 0 ]]; do # install the python packages in IsaacLab/source directory echo "[INFO] Installing extensions inside the Isaac Lab repository..." python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) + pip_uninstall_command=$(extract_pip_uninstall_command) + # check if pytorch is installed and its version # install pytorch with cuda 12.8 for blackwell support - ensure_cuda_torch ${python_exe} + ensure_cuda_torch # recursively look into directories and install them # this does not check dependencies between extensions export -f extract_python_exe + export -f extract_pip_command + export -f extract_pip_uninstall_command export -f install_isaaclab_extension # source directory find -L "${ISAACLAB_PATH}/source" -mindepth 1 -maxdepth 1 -type d -exec bash -c 'install_isaaclab_extension "{}"' \; @@ -411,12 +512,12 @@ while [[ $# -gt 0 ]]; do shift # past argument fi # install the learning frameworks specified - ${python_exe} -m pip install -e ${ISAACLAB_PATH}/source/isaaclab_rl["${framework_name}"] - ${python_exe} -m pip install -e ${ISAACLAB_PATH}/source/isaaclab_mimic["${framework_name}"] + ${pip_command} -e "${ISAACLAB_PATH}/source/isaaclab_rl[${framework_name}]" + ${pip_command} -e "${ISAACLAB_PATH}/source/isaaclab_mimic[${framework_name}]" # in some rare cases, torch might not be installed properly by setup.py, add one more check here # can prevent that from happening - ensure_cuda_torch ${python_exe} + ensure_cuda_torch # check if we are inside a docker container or are building a docker image # in that case don't setup VSCode since it asks for EULA agreement which triggers user interaction if is_docker; then @@ -427,8 +528,10 @@ while [[ $# -gt 0 ]]; do update_vscode_settings fi - # unset local variables + # unset local variables unset extract_python_exe + unset extract_pip_command + unset extract_pip_uninstall_command unset install_isaaclab_extension shift # past argument ;; @@ -446,11 +549,25 @@ while [[ $# -gt 0 ]]; do setup_conda_env ${conda_env_name} shift # past argument ;; + -u|--uv) + # use default name if not provided + if [ -z "$2" ]; then + echo "[INFO] Using default uv environment name: env_isaaclab" + uv_env_name="env_isaaclab" + else + echo "[INFO] Using uv environment name: $2" + uv_env_name=$2 + shift # past argument + fi + # setup the uv environment for Isaac Lab + setup_uv_env ${uv_env_name} + shift # past argument + ;; -f|--format) # reset the python path to avoid conflicts with pre-commit # this is needed because the pre-commit hooks are installed in a separate virtual environment # and it uses the system python to run the hooks - if [ -n "${CONDA_DEFAULT_ENV}" ]; then + if [ -n "${CONDA_DEFAULT_ENV}" ] || [ -n "${VIRTUAL_ENV}" ]; then cache_pythonpath=${PYTHONPATH} export PYTHONPATH="" fi @@ -458,7 +575,8 @@ while [[ $# -gt 0 ]]; do # check if pre-commit is installed if ! command -v pre-commit &>/dev/null; then echo "[INFO] Installing pre-commit..." - pip install pre-commit + pip_command=$(extract_pip_command) + ${pip_command} pre-commit sudo apt-get install -y pre-commit fi # always execute inside the Isaac Lab directory @@ -467,7 +585,7 @@ while [[ $# -gt 0 ]]; do pre-commit run --all-files cd - > /dev/null # set the python path back to the original value - if [ -n "${CONDA_DEFAULT_ENV}" ]; then + if [ -n "${CONDA_DEFAULT_ENV}" ] || [ -n "${VIRTUAL_ENV}" ]; then export PYTHONPATH=${cache_pythonpath} fi shift # past argument @@ -495,9 +613,10 @@ while [[ $# -gt 0 ]]; do -n|--new) # run the template generator script python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) shift # past argument echo "[INFO] Installing template dependencies..." - ${python_exe} -m pip install -q -r ${ISAACLAB_PATH}/tools/template/requirements.txt + ${pip_command} -q -r ${ISAACLAB_PATH}/tools/template/requirements.txt echo -e "\n[INFO] Running template generator...\n" ${python_exe} ${ISAACLAB_PATH}/tools/template/cli.py $@ # exit neatly @@ -532,9 +651,10 @@ while [[ $# -gt 0 ]]; do echo "[INFO] Building documentation..." # retrieve the python executable python_exe=$(extract_python_exe) + pip_command=$(extract_pip_command) # install pip packages cd ${ISAACLAB_PATH}/docs - ${python_exe} -m pip install -r requirements.txt > /dev/null + ${pip_command} -r requirements.txt > /dev/null # build the documentation ${python_exe} -m sphinx -b html -d _build/doctrees . _build/current # open the documentation diff --git a/tools/template/templates/external/README.md b/tools/template/templates/external/README.md index eeef1f3f87e..3b11b5407a8 100644 --- a/tools/template/templates/external/README.md +++ b/tools/template/templates/external/README.md @@ -15,7 +15,7 @@ It allows you to develop in an isolated environment, outside of the core Isaac L ## Installation - 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. + We recommend using the conda or uv installation as it simplifies calling Python scripts from the terminal. - Clone or copy this project/repository separately from the Isaac Lab installation (i.e. outside the `IsaacLab` directory): From 802ec5b7df771be1b91bc6744b2442eaa8f458df Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Thu, 11 Sep 2025 13:28:45 -0700 Subject: [PATCH 30/50] Moves IO descriptor log dir to logs (#3434) # Description Since we recently added logdir into the environment cfg, we can also move the IO description directory to be a subfolder under logdir. ## Type of change - Breaking change (existing functionality will not work without user modification) ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- scripts/reinforcement_learning/rl_games/train.py | 3 +-- scripts/reinforcement_learning/rsl_rl/train.py | 3 +-- scripts/reinforcement_learning/sb3/train.py | 3 +-- scripts/reinforcement_learning/skrl/train.py | 3 +-- source/isaaclab/config/extension.toml | 2 +- source/isaaclab/docs/CHANGELOG.rst | 9 +++++++++ source/isaaclab/isaaclab/envs/manager_based_env.py | 13 +++++++------ .../isaaclab/isaaclab/envs/manager_based_env_cfg.py | 3 --- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/scripts/reinforcement_learning/rl_games/train.py b/scripts/reinforcement_learning/rl_games/train.py index 59fb9144a4d..4d128ce420a 100644 --- a/scripts/reinforcement_learning/rl_games/train.py +++ b/scripts/reinforcement_learning/rl_games/train.py @@ -156,10 +156,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen obs_groups = agent_cfg["params"]["env"].get("obs_groups") concate_obs_groups = agent_cfg["params"]["env"].get("concate_obs_groups", True) - # set the IO descriptors output directory if requested + # set the IO descriptors export flag if requested if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors - env_cfg.io_descriptors_output_dir = os.path.join(log_root_path, log_dir) else: omni.log.warn( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." diff --git a/scripts/reinforcement_learning/rsl_rl/train.py b/scripts/reinforcement_learning/rsl_rl/train.py index ea630c5988d..b2056551f0c 100644 --- a/scripts/reinforcement_learning/rsl_rl/train.py +++ b/scripts/reinforcement_learning/rsl_rl/train.py @@ -141,10 +141,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen log_dir += f"_{agent_cfg.run_name}" log_dir = os.path.join(log_root_path, log_dir) - # set the IO descriptors output directory if requested + # set the IO descriptors export flag if requested if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors - env_cfg.io_descriptors_output_dir = log_dir else: omni.log.warn( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." diff --git a/scripts/reinforcement_learning/sb3/train.py b/scripts/reinforcement_learning/sb3/train.py index b2a9832a117..a87f728d9ae 100644 --- a/scripts/reinforcement_learning/sb3/train.py +++ b/scripts/reinforcement_learning/sb3/train.py @@ -143,10 +143,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen policy_arch = agent_cfg.pop("policy") n_timesteps = agent_cfg.pop("n_timesteps") - # set the IO descriptors output directory if requested + # set the IO descriptors export flag if requested if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors - env_cfg.io_descriptors_output_dir = log_dir else: omni.log.warn( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." diff --git a/scripts/reinforcement_learning/skrl/train.py b/scripts/reinforcement_learning/skrl/train.py index c66efae0138..83bd49f94f9 100644 --- a/scripts/reinforcement_learning/skrl/train.py +++ b/scripts/reinforcement_learning/skrl/train.py @@ -173,10 +173,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen # get checkpoint path (to resume training) resume_path = retrieve_file_path(args_cli.checkpoint) if args_cli.checkpoint else None - # set the IO descriptors output directory if requested + # set the IO descriptors export flag if requested if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors - env_cfg.io_descriptors_output_dir = os.path.join(log_root_path, log_dir) else: omni.log.warn( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." diff --git a/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml index 9d22bb9f692..b729ef46151 100644 --- a/source/isaaclab/config/extension.toml +++ b/source/isaaclab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.46.0" +version = "0.46.1" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index c8eaee1d724..64074cf542e 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog --------- +0.46.1 (2025-09-10) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Moved IO descriptors output directory to a subfolder under the task log directory. + + 0.46.0 (2025-09-06) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab/isaaclab/envs/manager_based_env.py b/source/isaaclab/isaaclab/envs/manager_based_env.py index 19b6bb2965d..9ddc538aa41 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_env.py @@ -250,12 +250,13 @@ def export_IO_descriptors(self, output_dir: str | None = None): IO_descriptors = self.get_IO_descriptors if output_dir is None: - output_dir = self.cfg.io_descriptors_output_dir - if output_dir is None: - raise ValueError( - "Output directory is not set. Please set the output directory using the `io_descriptors_output_dir`" - " configuration." - ) + if self.cfg.log_dir is not None: + output_dir = os.path.join(self.cfg.log_dir, "io_descriptors") + else: + raise ValueError( + "Output directory is not set. Please set the log directory using the `log_dir`" + " configuration or provide an explicit output_dir parameter." + ) if not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) diff --git a/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py b/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py index 1a6bdacb795..a7200a3d1d2 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/manager_based_env_cfg.py @@ -129,8 +129,5 @@ class ManagerBasedEnvCfg: export_io_descriptors: bool = False """Whether to export the IO descriptors for the environment. Defaults to False.""" - io_descriptors_output_dir: str | None = None - """The directory to export the IO descriptors to. Defaults to None.""" - log_dir: str | None = None """Directory for logging experiment artifacts. Defaults to None, in which case no specific log directory is set.""" From 383001916979fe542d3fafd01a3a1dea9c48b070 Mon Sep 17 00:00:00 2001 From: Robin Vishen <117207232+vi7n@users.noreply.github.com> Date: Fri, 12 Sep 2025 02:07:45 -0700 Subject: [PATCH 31/50] Fixes sign in DigitalFilter documentation (#3313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Fixes #3293 Corrects the documentation for implementing a low-pass filter with DigitalFilter. The coefficient A should be [-α] not [α] because DigitalFilter subtracts the recursive term (Y*A) in its implementation. ## Type of change - Bug fix (non-breaking change which fixes an issue) - This change requires a documentation update ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there Co-authored-by: James Tigue <166445701+jtigue-bdai@users.noreply.github.com> --- .../isaaclab/isaaclab/utils/modifiers/modifier.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/source/isaaclab/isaaclab/utils/modifiers/modifier.py b/source/isaaclab/isaaclab/utils/modifiers/modifier.py index efff7b4d8c9..fcbcd75cccd 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/modifier.py +++ b/source/isaaclab/isaaclab/utils/modifiers/modifier.py @@ -123,11 +123,13 @@ class DigitalFilter(ModifierBase): where :math:`\alpha` is a smoothing parameter between 0 and 1. Typically, the value of :math:`\alpha` is chosen based on the desired cut-off frequency of the filter. - This filter can be implemented as a digital filter with the coefficients :math:`A = [\alpha]` and + This filter can be implemented as a digital filter with the coefficients :math:`A = [-\alpha]` and :math:`B = [1 - \alpha]`. """ - def __init__(self, cfg: modifier_cfg.DigitalFilterCfg, data_dim: tuple[int, ...], device: str) -> None: + def __init__( + self, cfg: modifier_cfg.DigitalFilterCfg, data_dim: tuple[int, ...], device: str + ) -> None: """Initializes digital filter. Args: @@ -141,7 +143,9 @@ def __init__(self, cfg: modifier_cfg.DigitalFilterCfg, data_dim: tuple[int, ...] """ # check that filter coefficients are not None if cfg.A is None or cfg.B is None: - raise ValueError("Digital filter coefficients A and B must not be None. Please provide valid coefficients.") + raise ValueError( + "Digital filter coefficients A and B must not be None. Please provide valid coefficients." + ) # initialize parent class super().__init__(cfg, data_dim, device) @@ -213,7 +217,9 @@ class Integrator(ModifierBase): :math:`\Delta t` is the time step between samples. """ - def __init__(self, cfg: modifier_cfg.IntegratorCfg, data_dim: tuple[int, ...], device: str): + def __init__( + self, cfg: modifier_cfg.IntegratorCfg, data_dim: tuple[int, ...], device: str + ): """Initializes the integrator configuration and state. Args: From dd011a043e51dd14851bd4d5eb8a52cef7ac28a4 Mon Sep 17 00:00:00 2001 From: "-T.K.-" Date: Fri, 12 Sep 2025 02:10:37 -0700 Subject: [PATCH 32/50] Fixes ViewportCameraController numpy array missing datatype (#3375) # Description This PR fixes #3374. The numpy array type is specified upon creation to be fp32. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../isaaclab/envs/ui/viewport_camera_controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py b/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py index 15fc6817418..94ecdbc2461 100644 --- a/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py +++ b/source/isaaclab/isaaclab/envs/ui/viewport_camera_controller.py @@ -52,8 +52,8 @@ def __init__(self, env: ManagerBasedEnv | DirectRLEnv, cfg: ViewerCfg): self._env = env self._cfg = copy.deepcopy(cfg) # cast viewer eye and look-at to numpy arrays - self.default_cam_eye = np.array(self._cfg.eye) - self.default_cam_lookat = np.array(self._cfg.lookat) + self.default_cam_eye = np.array(self._cfg.eye, dtype=float) + self.default_cam_lookat = np.array(self._cfg.lookat, dtype=float) # set the camera origins if self.cfg.origin_type == "env": @@ -207,9 +207,9 @@ def update_view_location(self, eye: Sequence[float] | None = None, lookat: Seque """ # store the camera view pose for later use if eye is not None: - self.default_cam_eye = np.asarray(eye) + self.default_cam_eye = np.asarray(eye, dtype=float) if lookat is not None: - self.default_cam_lookat = np.asarray(lookat) + self.default_cam_lookat = np.asarray(lookat, dtype=float) # set the camera locations viewer_origin = self.viewer_origin.detach().cpu().numpy() cam_eye = viewer_origin + self.default_cam_eye From 7006bb75b111848f4e3e080ac79c6fe9692fc459 Mon Sep 17 00:00:00 2001 From: Mayank Mittal Date: Fri, 12 Sep 2025 15:41:03 +0200 Subject: [PATCH 33/50] Runs formatter to fix lint issues --- source/isaaclab/isaaclab/utils/modifiers/modifier.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/source/isaaclab/isaaclab/utils/modifiers/modifier.py b/source/isaaclab/isaaclab/utils/modifiers/modifier.py index fcbcd75cccd..6121d69ed1f 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/modifier.py +++ b/source/isaaclab/isaaclab/utils/modifiers/modifier.py @@ -127,9 +127,7 @@ class DigitalFilter(ModifierBase): :math:`B = [1 - \alpha]`. """ - def __init__( - self, cfg: modifier_cfg.DigitalFilterCfg, data_dim: tuple[int, ...], device: str - ) -> None: + def __init__(self, cfg: modifier_cfg.DigitalFilterCfg, data_dim: tuple[int, ...], device: str): """Initializes digital filter. Args: @@ -143,9 +141,7 @@ def __init__( """ # check that filter coefficients are not None if cfg.A is None or cfg.B is None: - raise ValueError( - "Digital filter coefficients A and B must not be None. Please provide valid coefficients." - ) + raise ValueError("Digital filter coefficients A and B must not be None. Please provide valid coefficients.") # initialize parent class super().__init__(cfg, data_dim, device) @@ -217,9 +213,7 @@ class Integrator(ModifierBase): :math:`\Delta t` is the time step between samples. """ - def __init__( - self, cfg: modifier_cfg.IntegratorCfg, data_dim: tuple[int, ...], device: str - ): + def __init__(self, cfg: modifier_cfg.IntegratorCfg, data_dim: tuple[int, ...], device: str): """Initializes the integrator configuration and state. Args: From 661117d40bd2aad865c502e2d46543f93dedda71 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Mon, 15 Sep 2025 12:04:29 -0700 Subject: [PATCH 34/50] Fixes missing actuator indices variable in joint randomization (#3447) # Description This PR fixes an issue introduced in #3251 , which accidentally deleted actuator_indices in the case where `asset_cfg.joint_id` is not a `slice` and `actuator.joint_indices` is a `slice` ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- source/isaaclab/config/extension.toml | 2 +- source/isaaclab/docs/CHANGELOG.rst | 10 ++++++++++ source/isaaclab/isaaclab/envs/mdp/events.py | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/source/isaaclab/config/extension.toml b/source/isaaclab/config/extension.toml index b729ef46151..fde6cb9bf91 100644 --- a/source/isaaclab/config/extension.toml +++ b/source/isaaclab/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.46.1" +version = "0.46.2" # Description title = "Isaac Lab framework for Robot Learning" diff --git a/source/isaaclab/docs/CHANGELOG.rst b/source/isaaclab/docs/CHANGELOG.rst index 64074cf542e..c33e307eebe 100644 --- a/source/isaaclab/docs/CHANGELOG.rst +++ b/source/isaaclab/docs/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog --------- + +0.46.2 (2025-09-13) +~~~~~~~~~~~~~~~~~~~ + +Changed +^^^^^^^ + +* Fixed missing actuator indices in :meth:`~isaaclab.envs.mdp.events.randomize_actuator_gains` + + 0.46.1 (2025-09-10) ~~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py index 5a6ec632d08..5c0c967e840 100644 --- a/source/isaaclab/isaaclab/envs/mdp/events.py +++ b/source/isaaclab/isaaclab/envs/mdp/events.py @@ -602,7 +602,7 @@ def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: raise TypeError("Actuator joint indices must be a slice or a torch.Tensor.") elif isinstance(actuator.joint_indices, slice): # we take the joints defined in the asset config - global_indices = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) + global_indices = actuator_indices = torch.tensor(self.asset_cfg.joint_ids, device=self.asset.device) else: # we take the intersection of the actuator joints and the asset config joints actuator_joint_indices = actuator.joint_indices From f1f0a8b0209db677e8b8953582db6c961c6cec21 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Tue, 16 Sep 2025 05:14:56 -0700 Subject: [PATCH 35/50] Fixes broken link in environment.rst for Dexsuite envs (#3470) # Description This fixes the broken link in documentation to the rl environment related to dexsuite Fixes #3460 # Type of Change - Bug fix (non-breaking change which fixes an issue) - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- docs/source/overview/environments.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index 72dcbff1851..af8d0ce84a4 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -188,8 +188,8 @@ for the lift-cube environment: .. |short-suction-link| replace:: `Isaac-Stack-Cube-UR10-Short-Suction-IK-Rel-v0 `__ .. |gr1_pp_waist-link| replace:: `Isaac-PickPlace-GR1T2-WaistEnabled-Abs-v0 `__ .. |galbot_stack-link| replace:: `Isaac-Stack-Cube-Galbot-Left-Arm-Gripper-RmpFlow-v0 `__ -.. |kuka-allegro-lift-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Lift-v0 `__ -.. |kuka-allegro-reorient-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 `__ +.. |kuka-allegro-lift-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Lift-v0 `__ +.. |kuka-allegro-reorient-link| replace:: `Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 `__ .. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ .. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ .. |cube-shadow-lstm-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 `__ From 75b67154b96ab839a8ee9352b19115719e8cfb84 Mon Sep 17 00:00:00 2001 From: rebeccazhang0707 <168459200+rebeccazhang0707@users.noreply.github.com> Date: Wed, 17 Sep 2025 07:46:32 +0800 Subject: [PATCH 36/50] Fixes errors in manipulation envs (#3418) # Description The CI tests met asset errors and controller errors for below envs: - "Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0", - "Isaac-Stack-Cube-Instance-Randomize-Franka-v0", - "Isaac-Place-Mug-Agibot-Left-Arm-RmpFlow-v0", - "Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0" ## Fixes - Add missing gripper configs in Stack TaskEnvCfgs: self.gripper_joint_names = ["panda_finger_.*"] self.gripper_open_val = 0.04 self.gripper_threshold = 0.005 - Move all object assets in Agibot tasks to S3 bucket. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo --- .../agibot/place_toy2box_rmp_rel_env_cfg.py | 26 +++++-------------- .../place_upright_mug_rmp_rel_env_cfg.py | 6 ++--- .../franka/bin_stack_joint_pos_env_cfg.py | 4 +++ .../config/franka/stack_joint_pos_env_cfg.py | 1 + ...ck_joint_pos_instance_randomize_env_cfg.py | 4 +++ 5 files changed, 17 insertions(+), 24 deletions(-) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py index 63f9f193186..0fa7fd9cafa 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py @@ -6,8 +6,6 @@ import os from dataclasses import MISSING -from isaaclab_assets import ISAACLAB_ASSETS_DATA_DIR - from isaaclab.assets import AssetBaseCfg, RigidObjectCfg from isaaclab.devices.device_base import DevicesCfg from isaaclab.devices.keyboard import Se3KeyboardCfg @@ -24,7 +22,7 @@ from isaaclab.sim.schemas.schemas_cfg import MassPropertiesCfg, RigidBodyPropertiesCfg from isaaclab.sim.spawners.from_files.from_files_cfg import UsdFileCfg from isaaclab.utils import configclass -from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab_tasks.manager_based.manipulation.place import mdp as place_mdp from isaaclab_tasks.manager_based.manipulation.stack import mdp @@ -57,7 +55,6 @@ class EventCfgPlaceToy2Box: "x": (-0.15, 0.20), "y": (-0.3, -0.15), "z": (-0.65, -0.65), - "roll": (1.57, 1.57), "yaw": (-3.14, 3.14), }, "asset_cfgs": [SceneEntityCfg("toy_truck")], @@ -71,7 +68,6 @@ class EventCfgPlaceToy2Box: "x": (0.25, 0.35), "y": (0.0, 0.10), "z": (-0.55, -0.55), - "roll": (1.57, 1.57), "yaw": (-3.14, 3.14), }, "asset_cfgs": [SceneEntityCfg("box")], @@ -267,14 +263,7 @@ def __post_init__(self): disable_gravity=False, ) - box_properties = RigidBodyPropertiesCfg( - solver_position_iteration_count=16, - solver_velocity_iteration_count=1, - max_angular_velocity=1000.0, - max_linear_velocity=1000.0, - max_depenetration_velocity=5.0, - disable_gravity=False, - ) + box_properties = toy_truck_properties.copy() # Notes: remember to add Physics/Mass properties to the toy_truck mesh to make grasping successful, # then you can use below MassPropertiesCfg to set the mass of the toy_truck @@ -286,8 +275,7 @@ def __post_init__(self): prim_path="{ENV_REGEX_NS}/ToyTruck", init_state=RigidObjectCfg.InitialStateCfg(), spawn=UsdFileCfg( - usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Objects/toy_truck_022.usd", - scale=(0.001, 0.001, 0.001), + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Objects/ToyTruck/toy_truck.usd", rigid_props=toy_truck_properties, mass_props=toy_mass_properties, ), @@ -297,9 +285,7 @@ def __post_init__(self): prim_path="{ENV_REGEX_NS}/Box", init_state=RigidObjectCfg.InitialStateCfg(), spawn=UsdFileCfg( - usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Objects/box_167.usd", - activate_contact_sensors=True, - scale=(0.001, 0.001, 0.001), + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Objects/Box/box.usd", rigid_props=box_properties, ), ) @@ -327,10 +313,10 @@ def __post_init__(self): # add contact force sensor for grasped checking self.scene.contact_grasp = ContactSensorCfg( prim_path="{ENV_REGEX_NS}/Robot/right_.*_Pad_Link", - update_period=0.0, + update_period=0.05, history_length=6, debug_vis=True, - filter_prim_paths_expr=["{ENV_REGEX_NS}/ToyTruck/Aligned"], + filter_prim_paths_expr=["{ENV_REGEX_NS}/ToyTruck"], ) self.teleop_devices = DevicesCfg( diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py index 4426eb423ce..6689a9cb154 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_upright_mug_rmp_rel_env_cfg.py @@ -6,8 +6,6 @@ import os from dataclasses import MISSING -from isaaclab_assets import ISAACLAB_ASSETS_DATA_DIR - from isaaclab.assets import AssetBaseCfg, RigidObjectCfg from isaaclab.devices.device_base import DevicesCfg from isaaclab.devices.keyboard import Se3KeyboardCfg @@ -23,7 +21,7 @@ from isaaclab.sim.schemas.schemas_cfg import RigidBodyPropertiesCfg from isaaclab.sim.spawners.from_files.from_files_cfg import UsdFileCfg from isaaclab.utils import configclass -from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab_tasks.manager_based.manipulation.place import mdp as place_mdp from isaaclab_tasks.manager_based.manipulation.place.config.agibot import place_toy2box_rmp_rel_env_cfg @@ -229,7 +227,7 @@ def __post_init__(self): prim_path="{ENV_REGEX_NS}/Mug", init_state=RigidObjectCfg.InitialStateCfg(), spawn=UsdFileCfg( - usd_path=f"{ISAACLAB_ASSETS_DATA_DIR}/Objects/mug.usd", + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Objects/Mug/mug.usd", scale=(1.0, 1.0, 1.0), rigid_props=mug_properties, ), diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py index fbc6454bba8..2952593df86 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/bin_stack_joint_pos_env_cfg.py @@ -113,6 +113,10 @@ def __post_init__(self): open_command_expr={"panda_finger_.*": 0.04}, close_command_expr={"panda_finger_.*": 0.0}, ) + # utilities for gripper status check + self.gripper_joint_names = ["panda_finger_.*"] + self.gripper_open_val = 0.04 + self.gripper_threshold = 0.005 # Rigid body properties of each cube cube_properties = RigidBodyPropertiesCfg( diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py index cc91754363d..ae01d277ba5 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_env_cfg.py @@ -87,6 +87,7 @@ def __post_init__(self): open_command_expr={"panda_finger_.*": 0.04}, close_command_expr={"panda_finger_.*": 0.0}, ) + # utilities for gripper status check self.gripper_joint_names = ["panda_finger_.*"] self.gripper_open_val = 0.04 self.gripper_threshold = 0.005 diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py index 51e2ecb8cc8..5ac1e9e2d2b 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/stack/config/franka/stack_joint_pos_instance_randomize_env_cfg.py @@ -87,6 +87,10 @@ def __post_init__(self): open_command_expr={"panda_finger_.*": 0.04}, close_command_expr={"panda_finger_.*": 0.0}, ) + # utilities for gripper status check + self.gripper_joint_names = ["panda_finger_.*"] + self.gripper_open_val = 0.04 + self.gripper_threshold = 0.005 # Rigid body properties of each cube cube_properties = RigidBodyPropertiesCfg( From 6807941891590d220b1917490b6afbf250e08b23 Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Wed, 17 Sep 2025 10:27:48 -0700 Subject: [PATCH 37/50] Fixes environment tests and disables bad ones (#3413) # Description Some visuomotor environments cannot run with 32 environments due to memory limitations. We disable those tests along with a few bad environments that need to be fixed. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Rebecca Zhang --- .github/workflows/build.yml | 1 + source/isaaclab_tasks/test/env_test_utils.py | 6 ++++-- tools/test_settings.py | 11 +++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a93264c2ac..1f32e55e79e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ on: branches: - devel - main + - 'release/**' # Concurrency control to prevent parallel runs on the same PR concurrency: diff --git a/source/isaaclab_tasks/test/env_test_utils.py b/source/isaaclab_tasks/test/env_test_utils.py index 23a92bab9c1..1034fd9ac92 100644 --- a/source/isaaclab_tasks/test/env_test_utils.py +++ b/source/isaaclab_tasks/test/env_test_utils.py @@ -123,11 +123,13 @@ def _run_environments( "Isaac-Stack-Cube-Franka-IK-Rel-Blueprint-v0", "Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0", "Isaac-Stack-Cube-Instance-Randomize-Franka-v0", - "Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-v0", - "Isaac-Stack-Cube-Franka-IK-Rel-Visuomotor-Cosmos-v0", ]: return + # skip these environments as they cannot be run with 32 environments within reasonable VRAM + if "Visuomotor" in task_name and num_envs == 32: + return + # skip automate environments as they require cuda installation if task_name in ["Isaac-AutoMate-Assembly-Direct-v0", "Isaac-AutoMate-Disassembly-Direct-v0"]: return diff --git a/tools/test_settings.py b/tools/test_settings.py index 936d38c4958..d6b81f1678b 100644 --- a/tools/test_settings.py +++ b/tools/test_settings.py @@ -18,17 +18,20 @@ PER_TEST_TIMEOUTS = { "test_articulation.py": 500, "test_stage_in_memory.py": 500, - "test_environments.py": 2000, # This test runs through all the environments for 100 steps each + "test_environments.py": 2500, # This test runs through all the environments for 100 steps each "test_environments_with_stage_in_memory.py": ( - 2000 + 2500 ), # Like the above, with stage in memory and with and without fabric cloning - "test_environment_determinism.py": 500, # This test runs through many the environments for 100 steps each + "test_environment_determinism.py": 1000, # This test runs through many the environments for 100 steps each "test_factory_environments.py": 1000, # This test runs through Factory environments for 100 steps each "test_multi_agent_environments.py": 800, # This test runs through multi-agent environments for 100 steps each "test_generate_dataset.py": 500, # This test runs annotation for 10 demos and generation until one succeeds - "test_environments_training.py": 6000, + "test_environments_training.py": ( + 6000 + ), # This test runs through training for several environments and compares thresholds "test_simulation_render_config.py": 500, "test_operational_space.py": 500, + "test_non_headless_launch.py": 1000, # This test launches the app in non-headless mode and starts simulation } """A dictionary of tests and their timeouts in seconds. From 137106d35aaa89fec41254209c96406ca580d745 Mon Sep 17 00:00:00 2001 From: peterd-NV Date: Wed, 17 Sep 2025 13:16:23 -0700 Subject: [PATCH 38/50] Updates dataset instruction in `teleop_imitation.rst` (#3462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Updates the Isaac Lab Mimic doc tutorials according to prior reviewer feedback. Changes: 1. Clarify demo success criteria for Demo 2 (humanoid pouring task) 2. Add visualization at the start of Demo 2 to clearly indicate that it is a different task from Demo 1. 3. Document the dataset of Demo 2 ## Type of change - Documentation update ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../imitation-learning/teleop_imitation.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/source/overview/imitation-learning/teleop_imitation.rst b/docs/source/overview/imitation-learning/teleop_imitation.rst index 859287560a8..84b2551f6dc 100644 --- a/docs/source/overview/imitation-learning/teleop_imitation.rst +++ b/docs/source/overview/imitation-learning/teleop_imitation.rst @@ -511,11 +511,17 @@ Visualize the results of the trained policy by running the following command, us Demo 2: Visuomotor Policy for a Humanoid Robot ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. figure:: https://download.isaacsim.omniverse.nvidia.com/isaaclab/images/gr-1_nut_pouring_policy.gif + :width: 100% + :align: center + :alt: GR-1 humanoid robot performing a pouring task + :figclass: align-center + Download the Dataset ^^^^^^^^^^^^^^^^^^^^ -Download the pre-generated dataset from `here `__ and place it under ``IsaacLab/datasets/generated_dataset_gr1_nut_pouring.hdf5``. -The dataset contains 1000 demonstrations of a humanoid robot performing a pouring/placing task that was +Download the pre-generated dataset from `here `__ and place it under ``IsaacLab/datasets/generated_dataset_gr1_nut_pouring.hdf5`` +(**Note: The dataset size is approximately 12GB**). The dataset contains 1000 demonstrations of a humanoid robot performing a pouring/placing task that was generated using Isaac Lab Mimic for the ``Isaac-NutPour-GR1T2-Pink-IK-Abs-Mimic-v0`` task. .. hint:: @@ -526,7 +532,11 @@ generated using Isaac Lab Mimic for the ``Isaac-NutPour-GR1T2-Pink-IK-Abs-Mimic- Then, it drops the red beaker into the blue bin. Lastly, it places the yellow bowl onto the white scale. See the video in the :ref:`visualize-results-demo-2` section below for a visual demonstration of the task. - **Note that the following commands are only for your reference and are not required for this demo.** + **The success criteria for this task requires the red beaker to be placed in the blue bin, the green nut to be in the yellow bowl, + and the yellow bowl to be placed on top of the white scale.** + + .. attention:: + **The following commands are only for your reference and are not required for this demo.** To collect demonstrations: From 8b590c1a72c5998c56be204f4d77d46b192b6e94 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:39:27 +0200 Subject: [PATCH 39/50] Details installation section in documentation (#3442) # Description It was getting confusing with the table of content on which installation instruction to follow when. This MR takes a stab at writing clearer documentation for users. I hope this helps with new users coming to Isaac Lab. ## Type of change - Documentation update ## Screenshots image ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- README.md | 86 --- docs/conf.py | 2 + .../setup/installation/asset_caching.rst | 31 +- .../installation/binaries_installation.rst | 7 +- docs/source/setup/installation/index.rst | 134 ++++- .../isaaclab_pip_installation.rst | 17 +- .../setup/installation/pip_installation.rst | 4 +- .../installation/source_installation.rst | 541 ++++++++++++++++++ 8 files changed, 686 insertions(+), 136 deletions(-) create mode 100644 docs/source/setup/installation/source_installation.rst diff --git a/README.md b/README.md index bd176eef6b2..5d509f80d49 100644 --- a/README.md +++ b/README.md @@ -37,92 +37,6 @@ Isaac Lab offers a comprehensive set of tools and environments designed to facil ## Getting Started -### Getting Started with Open-Source Isaac Sim - -Isaac Sim is now open source and available on GitHub! - -For detailed Isaac Sim installation instructions, please refer to -[Isaac Sim README](https://github.com/isaac-sim/IsaacSim?tab=readme-ov-file#quick-start). - -1. Clone Isaac Sim - - ``` - git clone https://github.com/isaac-sim/IsaacSim.git - ``` - -2. Build Isaac Sim - - ``` - cd IsaacSim - ./build.sh - ``` - - On Windows, please use `build.bat` instead. - -3. Clone Isaac Lab - - ``` - cd .. - git clone https://github.com/isaac-sim/IsaacLab.git - cd isaaclab - ``` - -4. Set up symlink in Isaac Lab - - Linux: - - ``` - ln -s ../IsaacSim/_build/linux-x86_64/release _isaac_sim - ``` - - Windows: - - ``` - mklink /D _isaac_sim ..\IsaacSim\_build\windows-x86_64\release - ``` - -5. Install Isaac Lab - - Linux: - - ``` - ./isaaclab.sh -i - ``` - - Windows: - - ``` - isaaclab.bat -i - ``` - -6. [Optional] Set up a virtual python environment (e.g. for Conda) - - Linux: - - ``` - source _isaac_sim/setup_conda_env.sh - ``` - - Windows: - - ``` - _isaac_sim\setup_python_env.bat - ``` - -7. Train! - - Linux: - - ``` - ./isaaclab.sh -p scripts/reinforcement_learning/skrl/train.py --task Isaac-Ant-v0 --headless - ``` - - Windows: - - ``` - isaaclab.bat -p scripts\reinforcement_learning\skrl\train.py --task Isaac-Ant-v0 --headless - ``` - ### Documentation Our [documentation page](https://isaac-sim.github.io/IsaacLab) provides everything you need to get started, including diff --git a/docs/conf.py b/docs/conf.py index 28580574b26..7c68e911018 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -96,6 +96,8 @@ (r"py:.*", r"trimesh.*"), # we don't have intersphinx mapping for trimesh ] +# emoji style +sphinxemoji_style = "twemoji" # options: "twemoji" or "unicode" # put type hints inside the signature instead of the description (easier to maintain) autodoc_typehints = "signature" # autodoc_typehints_format = "fully-qualified" diff --git a/docs/source/setup/installation/asset_caching.rst b/docs/source/setup/installation/asset_caching.rst index 5ee0760b681..4c67729bb6a 100644 --- a/docs/source/setup/installation/asset_caching.rst +++ b/docs/source/setup/installation/asset_caching.rst @@ -8,7 +8,7 @@ In some cases, it is possible that asset loading times can be long when assets a If you run into cases where assets take a few minutes to load for each run, we recommend enabling asset caching following the below steps. -First, launch the Isaac Sim app: +First, launch the Isaac Sim application: .. tab-set:: :sync-group: os @@ -27,25 +27,32 @@ First, launch the Isaac Sim app: isaaclab.bat -s -On the top right of the Isaac Sim app, there will be an icon labelled ``CACHE:``. -There may be a message indicating ``HUB NOT DETECTED`` or ``NEW VERSION DETECTED``. +On the top right of the Isaac Lab or Isaac Sim app, look for the icon labeled ``CACHE:``. +You may see a message such as ``HUB NOT DETECTED`` or ``NEW VERSION DETECTED``. + +Click the message to enable `Hub `_. +Hub automatically manages local caching for Isaac Lab assets, so subsequent runs will use cached files instead of +downloading from AWS each time. .. figure:: ../../_static/setup/asset_caching.jpg :align: center :figwidth: 100% :alt: Simulator with cache messaging. -Click on the message, which will enable `Hub `_ -for asset caching. Once enabled, Hub will run automatically each time an Isaac Lab or Isaac Sim instance is run. +Hub provides better control and management of cached assets, making workflows faster and more reliable, especially +in environments with limited or intermittent internet access. -Note that for the first run, assets will still need to be pulled from the cloud, which could lead to longer loading times. -However, subsequent runs that use the same assets will be able to use the cached files from Hub. -Hub will provide better control for caching of assets used in Isaac Lab. +.. note:: + The first time you run Isaac Lab, assets will still need to be pulled from the cloud, which could lead + to longer loading times. Once cached, loading times will be significantly reduced on subsequent runs. Nucleus ------- -Prior to Isaac Sim 4.5, assets were accessible from the Omniverse Nucleus server and through setting up a local Nucleus server. -Although from Isaac Sim 4.5, we have deprecated the use of Omniverse Nucleus and the Omniverse Launcher, any existing instances -or setups of local Nucleus instances should still work. We recommend keeping existing setups if a local Nucleus server -was previously already set up. + +Before Isaac Sim 4.5, assets were accessed via the Omniverse Nucleus server, including setups with local Nucleus instances. + +.. warning:: + Starting with Isaac Sim 4.5, the Omniverse Nucleus server and Omniverse Launcher are deprecated. + Existing Nucleus setups will continue to work, so if you have a local Nucleus server already configured, + you may continue to use it. diff --git a/docs/source/setup/installation/binaries_installation.rst b/docs/source/setup/installation/binaries_installation.rst index 2ffddda6ece..a3b32961ba2 100644 --- a/docs/source/setup/installation/binaries_installation.rst +++ b/docs/source/setup/installation/binaries_installation.rst @@ -3,7 +3,7 @@ Installation using Isaac Sim Binaries ===================================== -Isaac Lab requires Isaac Sim. This tutorial installs Isaac Sim first from binaries, then Isaac Lab from source code. +Isaac Lab requires Isaac Sim. This tutorial installs Isaac Sim first from its binaries, then Isaac Lab from source code. Installing Isaac Sim -------------------- @@ -15,7 +15,8 @@ Please follow the Isaac Sim `documentation `__ to install the latest Isaac Sim release. -From Isaac Sim 4.5 release, Isaac Sim binaries can be `downloaded `_ directly as a zip file. +From Isaac Sim 4.5 release, Isaac Sim binaries can be `downloaded `_ +directly as a zip file. To check the minimum system requirements, refer to the documentation `here `__. @@ -485,7 +486,7 @@ and ``source _isaac_sim/setup_conda_env.sh`` has been executed (for uv as well). Train a robot! -~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~ You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! We recommend adding ``--headless`` for faster training. diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst index 432866bf5ce..77cc994b3a1 100644 --- a/docs/source/setup/installation/index.rst +++ b/docs/source/setup/installation/index.rst @@ -19,6 +19,11 @@ Local Installation :target: https://www.microsoft.com/en-ca/windows/windows-11 :alt: Windows 11 + +Isaac Lab installation is available for Windows and Linux. Since it is built on top of Isaac Sim, +it is required to install Isaac Sim before installing Isaac Lab. This guide explains the +recommended installation methods for both Isaac Sim and Isaac Lab. + .. caution:: We have dropped support for Isaac Sim versions 4.2.0 and below. We recommend using the latest @@ -27,44 +32,121 @@ Local Installation For more information, please refer to the `Isaac Sim release notes `__. -.. note:: - We recommend system requirements with at least 32GB RAM and 16GB VRAM for Isaac Lab. - For workflows with rendering enabled, additional VRAM may be required. - For the full list of system requirements for Isaac Sim, please refer to the - `Isaac Sim system requirements `_. +System Requirements +------------------- + +General Requirements +~~~~~~~~~~~~~~~~~~~~ + +- **RAM:** 32 GB or more +- **GPU VRAM:** 16 GB or more (additional VRAM may be required for rendering workflows) +- **OS:** Ubuntu 22.04 (Linux x64) or Windows 11 (x64) + +For detailed requirements, see the +`Isaac Sim system requirements `_. + +Driver Requirements +~~~~~~~~~~~~~~~~~~~ + +Drivers other than those recommended on `Omniverse Technical Requirements `_ +may work but have not been validated against all Omniverse tests. + +- Use the **latest NVIDIA production branch driver**. +- On Linux, version ``535.216.01`` or later is recommended, especially when upgrading to + **Ubuntu 22.04.5 with kernel 6.8.0-48-generic** or newer. +- If you are using a new GPU or encounter driver issues, install the latest production branch + driver from the `Unix Driver Archive `_ + using the ``.run`` installer. + +Troubleshooting +~~~~~~~~~~~~~~~ + +Please refer to the `Linux Troubleshooting `_ +to resolve installation issues in Linux. + + +Quick Start (Recommended) +------------------------- + +For most users, the simplest and fastest way to install Isaac Lab is by following the +:doc:`pip_installation` guide. + +This method will install Isaac Sim via pip and Isaac Lab through its source code. +If you are new to Isaac Lab, start here. + + +Choosing an Installation Method +------------------------------- + +Different workflows require different installation methods. +Use this table to decide: + ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Method | Isaac Sim | Isaac Lab | Best For | Difficulty | ++===================+==============================+==============================+===========================+============+ +| **Recommended** | |:package:| pip install | |:floppy_disk:| source (git) | Beginners, standard use | Easy | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Binary + Source | |:inbox_tray:| binary | |:floppy_disk:| source (git) | Users preferring binary | Easy | +| | download | | install of Isaac Sim | | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Full Source Build | |:floppy_disk:| source (git) | |:floppy_disk:| source (git) | Developers modifying both | Advanced | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ +| Pip Only | |:package:| pip install | |:package:| pip install | External extensions only | Special | +| | | | (no training/examples) | case | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ + + +Next Steps +---------- + +Once you've reviewed the installation methods, continue with the guide that matches your workflow: + +- |:smiley:| :doc:`pip_installation` + + - Install Isaac Sim via pip and Isaac Lab from source. + - Best for beginners and most users. + +- :doc:`binaries_installation` + + - Install Isaac Sim from its binary package (website download). + - Install Isaac Lab from its source code. + - Choose this if you prefer not to use pip for Isaac Sim (for instance, on Ubuntu 20.04). - For details on driver requirements, please see the `Technical Requirements `_ guide +- :doc:`source_installation` - * See `Linux Troubleshooting `_ to resolve driver installation issues in linux - * If you are on a new GPU or are experiencing issues with the current drivers, we recommend installing the **latest production branch version** drivers from the `Unix Driver Archive `_ using the ``.run`` installer on Linux. - * NVIDIA driver version ``535.216.01`` or later is recommended when upgrading to **Ubuntu 22.04.5 kernel 6.8.0-48-generic** or later + - Build Isaac Sim from source. + - Install Isaac Lab from its source code. + - Recommended only if you plan to modify Isaac Sim itself. +- :doc:`isaaclab_pip_installation` -Isaac Lab is built on top of the Isaac Sim platform. Therefore, it is required to first install Isaac Sim -before using Isaac Lab. + - Install Isaac Sim and Isaac Lab as pip packages. + - Best for advanced users building **external extensions** with custom runner scripts. + - Note: This does **not** include training or example scripts. -Both Isaac Sim and Isaac Lab provide two ways of installation: -either through binary download/source file, or through Python's package installer ``pip``. -The method of installation may depend on the use case and the level of customization desired from users. -For example, installing Isaac Sim from pip will be a simpler process than installing it from binaries, -but the source code will then only be accessible through the installed source package and not through the direct binary download. +Asset Caching +------------- -Similarly, installing Isaac Lab through pip is only recommended for workflows that use external launch scripts outside of Isaac Lab. -The Isaac Lab pip packages only provide the core framework extensions for Isaac Lab and does not include any of the -standalone training, inferencing, and example scripts. Therefore, this workflow is recommended for projects that are -built as external extensions outside of Isaac Lab, which utilizes user-defined runner scripts. +Isaac Lab assets are hosted on **AWS S3 cloud storage**. Loading times can vary +depending on your **network connection** and **geographical location**, and in some cases, +assets may take several minutes to load for each run. To improve performance or support +**offline workflows**, we recommend enabling **asset caching**. -We recommend using Isaac Sim pip installation for a simplified installation experience. +- Cached assets are stored locally, reducing repeated downloads. +- This is especially useful if you have a slow or intermittent internet connection, + or if your deployment environment is offline. -For users getting started with Isaac Lab, we recommend installing Isaac Lab by cloning the repo. +Please follow the steps :doc:`asset_caching` to enable asset caching and speed up your workflow. .. toctree:: :maxdepth: 2 + :hidden: - Pip installation (recommended) - Binary installation - Advanced installation (Isaac Lab pip) - Asset caching + pip_installation + binaries_installation + source_installation + isaaclab_pip_installation + asset_caching diff --git a/docs/source/setup/installation/isaaclab_pip_installation.rst b/docs/source/setup/installation/isaaclab_pip_installation.rst index 153d575fd99..d9d9441ca2f 100644 --- a/docs/source/setup/installation/isaaclab_pip_installation.rst +++ b/docs/source/setup/installation/isaaclab_pip_installation.rst @@ -187,14 +187,17 @@ To run a user-defined script for Isaac Lab, simply run Generating VS Code Settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Due to the structure resulting from the installation, VS Code IntelliSense (code completion, parameter info and member lists, etc.) will not work by default. -To set it up (define the search paths for import resolution, the path to the default Python interpreter, and other settings), for a given workspace folder, run the following command: +Due to the structure resulting from the installation, VS Code IntelliSense (code completion, parameter info +and member lists, etc.) will not work by default. To set it up (define the search paths for import resolution, +the path to the default Python interpreter, and other settings), for a given workspace folder, +run the following command: - .. code-block:: bash +.. code-block:: bash - python -m isaaclab --generate-vscode-settings + python -m isaaclab --generate-vscode-settings - .. warning:: - The command will generate a ``.vscode/settings.json`` file in the workspace folder. - If the file already exists, it will be overwritten (a confirmation prompt will be shown first). +.. warning:: + + The command will generate a ``.vscode/settings.json`` file in the workspace folder. + If the file already exists, it will be overwritten (a confirmation prompt will be shown first). diff --git a/docs/source/setup/installation/pip_installation.rst b/docs/source/setup/installation/pip_installation.rst index 73d24b0fef8..1337df6d9cb 100644 --- a/docs/source/setup/installation/pip_installation.rst +++ b/docs/source/setup/installation/pip_installation.rst @@ -1,6 +1,6 @@ .. _isaaclab-pip-installation: -Installation using Isaac Sim pip +Installation using Isaac Sim Pip ================================ Isaac Lab requires Isaac Sim. This tutorial first installs Isaac Sim from pip, then Isaac Lab from source code. @@ -371,7 +371,7 @@ On Windows machines, please terminate the process from Command Prompt using If you see this, then the installation was successful! |:tada:| Train a robot! -~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~ You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! We recommend adding ``--headless`` for faster training. diff --git a/docs/source/setup/installation/source_installation.rst b/docs/source/setup/installation/source_installation.rst new file mode 100644 index 00000000000..7f7cd232154 --- /dev/null +++ b/docs/source/setup/installation/source_installation.rst @@ -0,0 +1,541 @@ +.. _isaaclab-source-installation: + +Installation using Isaac Sim Source +=================================== + +Isaac Lab requires Isaac Sim. This tutorial first installs Isaac Sim from source, then Isaac Lab from source code. + +.. note:: + + This is a more advanced installation method and is not recommended for most users. Only follow this method + if you wish to modify the source code of Isaac Sim as well. + +Installing Isaac Sim +-------------------- + +From Isaac Sim 5.0 release, it is possible to build Isaac Sim from its source code. +This approach is meant for users who wish to modify the source code of Isaac Sim as well, +or want to test Isaac Lab with the nightly version of Isaac Sim. + +The following instructions are adapted from the `Isaac Sim documentation `_ +for the convenience of users. + +.. attention:: + + Building Isaac Sim from source requires Ubuntu 22.04 LTS or higher. + +.. attention:: + + For details on driver requirements, please see the `Technical Requirements `_ guide! + + On Windows, it may be necessary to `enable long path support `_ to avoid installation errors due to OS limitations. + + +- Clone the Isaac Sim repository into your workspace: + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacSim.git + +- Build Isaac Sim from source: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + cd IsaacSim + ./build.sh + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: bash + + cd IsaacSim + build.bat + + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +To avoid the overhead of finding and locating the Isaac Sim installation +directory every time, we recommend exporting the following environment +variables to your terminal for the remaining of the installation instructions: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Isaac Sim root directory + export ISAACSIM_PATH="${pwd}/_build/linux-x86_64/release" + # Isaac Sim python executable + export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Isaac Sim root directory + set ISAACSIM_PATH="%cd%\_build\windows-x86_64\release" + :: Isaac Sim python executable + set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" + +- Check that the simulator runs as expected: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + ${ISAACSIM_PATH}/isaac-sim.sh + + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: note: you can pass the argument "--help" to see all arguments possible. + %ISAACSIM_PATH%\isaac-sim.bat + + +- Check that the simulator runs from a standalone python script: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # checks that python path is set correctly + ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')" + # checks that Isaac Sim can be launched from python + ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.api/add_cubes.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: checks that python path is set correctly + %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')" + :: checks that Isaac Sim can be launched from python + %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.api\add_cubes.py + +.. caution:: + + If you have been using a previous version of Isaac Sim, you need to run the following command for the *first* + time after installation to remove all the old user data and cached variables: + + .. tab-set:: + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + + .. code:: bash + + ${ISAACSIM_PATH}/isaac-sim.sh --reset-user + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + + .. code:: batch + + %ISAACSIM_PATH%\isaac-sim.bat --reset-user + + +If the simulator does not run or crashes while following the above +instructions, it means that something is incorrectly configured. To +debug and troubleshoot, please check Isaac Sim +`documentation `__ +and the +`forums `__. + + +Installing Isaac Lab +-------------------- + +Cloning Isaac Lab +~~~~~~~~~~~~~~~~~ + +.. note:: + + We recommend making a `fork `_ of the Isaac Lab repository to contribute + to the project but this is not mandatory to use the framework. If you + make a fork, please replace ``isaac-sim`` with your username + in the following instructions. + +Clone the Isaac Lab repository into your **workspace**. Please note that the location of the Isaac Lab repository +should be outside of the Isaac Sim repository. For example, if you cloned Isaac Sim into ``~/IsaacSim``, +then you should clone Isaac Lab into ``~/IsaacLab``. + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:isaac-sim/IsaacLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacLab.git + + +.. note:: + We provide a helper executable `isaaclab.sh `_ that provides + utilities to manage extensions: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: text + + ./isaaclab.sh --help + + usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. + -t, --test Run all python pytest tests. + -o, --docker Run the docker container helper script (docker/container.sh). + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -n, --new Create a new external project or internal task from template. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: text + + isaaclab.bat --help + + usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. + -t, --test Run all python pytest tests. + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -n, --new Create a new external project or internal task from template. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. + + +Creating the Isaac Sim Symbolic Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set up a symbolic link between the installed Isaac Sim root folder +and ``_isaac_sim`` in the Isaac Lab directory. This makes it convenient +to index the python modules and look for extensions shipped with Isaac Sim. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # enter the cloned repository + cd IsaacLab + # create a symbolic link + ln -s ${ISAACSIM_PATH} _isaac_sim + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: enter the cloned repository + cd IsaacLab + :: create a symbolic link - requires launching Command Prompt with Administrator access + mklink /D _isaac_sim ${ISAACSIM_PATH} + + +Setting up the uv environment (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attention:: + This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. + +The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac +Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable +behaves like a python executable, and can be used to run any python script or +module with the simulator. For more information, please refer to the +`documentation `__. + +To install ``uv``, please follow the instructions `here `__. +You can create the Isaac Lab environment using the following commands. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default name for uv environment is 'env_isaaclab' + ./isaaclab.sh --uv # or "./isaaclab.sh -u" + # Option 2: Custom name for uv environment + ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Default name for uv environment is 'env_isaaclab' + isaaclab.bat --uv :: or "isaaclab.bat -u" + :: Option 2: Custom name for uv environment + isaaclab.bat --uv my_env :: or "isaaclab.bat -u my_env" + + +Once created, be sure to activate the environment before proceeding! + +.. code:: bash + + source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" + +Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` +to run python scripts. You can use the default python executable in your environment +by running ``python`` or ``python3``. However, for the rest of the documentation, +we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command +is equivalent to running ``python`` or ``python3`` in your virtual environment. + + +Setting up the conda environment (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attention:: + This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. + +.. note:: + + If you use Conda, we recommend using `Miniconda `_. + +The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac +Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable +behaves like a python executable, and can be used to run any python script or +module with the simulator. For more information, please refer to the +`documentation `__. + +To install ``conda``, please follow the instructions `here `__. +You can create the Isaac Lab environment using the following commands. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default name for conda environment is 'env_isaaclab' + ./isaaclab.sh --conda # or "./isaaclab.sh -c" + # Option 2: Custom name for conda environment + ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Default name for conda environment is 'env_isaaclab' + isaaclab.bat --conda :: or "isaaclab.bat -c" + :: Option 2: Custom name for conda environment + isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" + + +Once created, be sure to activate the environment before proceeding! + +.. code:: bash + + conda activate env_isaaclab # or "conda activate my_env" + +Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` +to run python scripts. You can use the default python executable in your environment +by running ``python`` or ``python3``. However, for the rest of the documentation, +we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command +is equivalent to running ``python`` or ``python3`` in your virtual environment. + +Installation +~~~~~~~~~~~~ + +- Install dependencies using ``apt`` (on Ubuntu): + + .. code:: bash + + sudo apt install cmake build-essential + +- Run the install command that iterates over all the extensions in ``source`` directory and installs them + using pip (with ``--editable`` flag): + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install # or "./isaaclab.sh -i" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: bash + + isaaclab.bat --install :: or "isaaclab.bat -i" + +.. note:: + + By default, this will install all the learning frameworks. If you want to install only a specific framework, you can + pass the name of the framework as an argument. For example, to install only the ``rl_games`` framework, you can run + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: bash + + isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" + + The valid options are ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, ``none``. + + +Verifying the Isaac Lab installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To verify that the installation was successful, run the following command from the +top of the repository: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Using the isaaclab.sh executable + # note: this works for both the bundled python and the virtual environment + ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py + + # Option 2: Using python in your virtual environment + python scripts/tutorials/00_sim/create_empty.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Using the isaaclab.bat executable + :: note: this works for both the bundled python and the virtual environment + isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py + + :: Option 2: Using python in your virtual environment + python scripts\tutorials\00_sim\create_empty.py + + +The above command should launch the simulator and display a window with a black +viewport as shown below. You can exit the script by pressing ``Ctrl+C`` on your terminal. +On Windows machines, please terminate the process from Command Prompt using +``Ctrl+Break`` or ``Ctrl+fn+B``. + + +.. figure:: ../../_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + +If you see this, then the installation was successful! |:tada:| + +Train a robot! +~~~~~~~~~~~~~~ + +You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! +We recommend adding ``--headless`` for faster training. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless + +... Or a robot dog! + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless + +Isaac Lab provides the tools you'll need to create your own **Tasks** and **Workflows** for whatever your project needs may be. Take a look at our :ref:`how-to` guides like `Adding your own learning Library `_ or `Wrapping Environments `_ for details. + +.. figure:: ../../_static/setup/isaac_ants_example.jpg + :align: center + :figwidth: 100% + :alt: Idle hands... From ced52cd9871b800ebb326504fcefa895fb1026e5 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Wed, 17 Sep 2025 17:47:32 -0700 Subject: [PATCH 40/50] Removes extra calls to write simulation after reset_idx (#3446) # Description This PR removes the calls into write simulation after reset_idx, I think this is not needed anymore because of PR #2736 Lets see if it passes all test Fixes # (issue) ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo --- source/isaaclab/isaaclab/envs/direct_rl_env.py | 3 --- source/isaaclab/isaaclab/envs/manager_based_rl_env.py | 3 --- source/isaaclab/test/assets/test_articulation.py | 1 - 3 files changed, 7 deletions(-) diff --git a/source/isaaclab/isaaclab/envs/direct_rl_env.py b/source/isaaclab/isaaclab/envs/direct_rl_env.py index e985c2c6531..e43c4db7a28 100644 --- a/source/isaaclab/isaaclab/envs/direct_rl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_rl_env.py @@ -376,9 +376,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) if len(reset_env_ids) > 0: self._reset_idx(reset_env_ids) - # update articulation kinematics - self.scene.write_data_to_sim() - self.sim.forward() # if sensors are added to the scene, make sure we render to reflect changes in reset if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: self.sim.render() diff --git a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py index 118f588c100..634bec4cae9 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py @@ -220,9 +220,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: self.recorder_manager.record_pre_reset(reset_env_ids) self._reset_idx(reset_env_ids) - # update articulation kinematics - self.scene.write_data_to_sim() - self.sim.forward() # if sensors are added to the scene, make sure we render to reflect changes in reset if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: diff --git a/source/isaaclab/test/assets/test_articulation.py b/source/isaaclab/test/assets/test_articulation.py index 30a97b3275c..46eb63762fd 100644 --- a/source/isaaclab/test/assets/test_articulation.py +++ b/source/isaaclab/test/assets/test_articulation.py @@ -1879,7 +1879,6 @@ def test_write_joint_state_data_consistency(sim, num_articulations, device, grav rand_joint_vel = vel_dist.sample() articulation.write_joint_state_to_sim(rand_joint_pos, rand_joint_vel) - articulation.root_physx_view.get_jacobians() # make sure valued updated assert torch.count_nonzero(original_body_states[:, 1:] != articulation.data.body_state_w[:, 1:]) > ( len(original_body_states[:, 1:]) / 2 From 7004eb1436a45770460f32805a84db39dbe8a0b6 Mon Sep 17 00:00:00 2001 From: Alexander Poddubny <143108850+nv-apoddubny@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:48:06 -0700 Subject: [PATCH 41/50] Applies the pre-merge CI failure control to the tasks (#3457) Applied the pre-merge CI failure control to the tasks stage, as it is done in the general test stage Co-authored-by: Kelly Guo --- .github/workflows/build.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f32e55e79e..8c56cacc435 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,6 +76,20 @@ jobs: retention-days: 1 compression-level: 9 + - name: Check Test Results for Fork PRs + if: github.event.pull_request.head.repo.full_name != github.repository + run: | + if [ -f "reports/isaaclab-tasks-report.xml" ]; then + # Check if the test results contain any failures + if grep -q 'failures="[1-9]' reports/isaaclab-tasks-report.xml || grep -q 'errors="[1-9]' reports/isaaclab-tasks-report.xml; then + echo "Tests failed for PR from fork. The test report is in the logs. Failing the job." + exit 1 + fi + else + echo "No test results file found. This might indicate test execution failed." + exit 1 + fi + test-general: runs-on: [self-hosted, gpu] timeout-minutes: 180 From 67921815ae10fda82ee988adce46f4f3e25d4301 Mon Sep 17 00:00:00 2001 From: njawale42 Date: Wed, 17 Sep 2025 17:53:36 -0700 Subject: [PATCH 42/50] Updates the Path to Isaaclab Dir in SkillGen Documentation (#3482) # Updates the path to IsaacLab directory in SkillGen documentation ## Description This PR updates the IsaacLab workspace path for clarity and consistency in SkillGen documentation. - Updated `docs/source/overview/imitation-learning/skillgen.rst` (Download and Setup section) - Replaced: - Before: `cd /path/to/your/isaaclab/root` - After: `cd /path/to/your/IsaacLab` Motivation: Aligns with the repository name and common workspace convention, reducing confusion and preventing copy-paste errors for users following setup steps. Dependencies: None ## Type of change - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- docs/source/overview/imitation-learning/skillgen.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/overview/imitation-learning/skillgen.rst b/docs/source/overview/imitation-learning/skillgen.rst index 28d2dbe5805..b6a873ee63c 100644 --- a/docs/source/overview/imitation-learning/skillgen.rst +++ b/docs/source/overview/imitation-learning/skillgen.rst @@ -124,7 +124,7 @@ Download and Setup .. code:: bash # Make sure you are in the root directory of your Isaac Lab workspace - cd /path/to/your/isaaclab/root + cd /path/to/your/IsaacLab # Create the datasets directory if it does not exist mkdir -p datasets From 0d1725e1c74411fbb136e7766b5c22f1c899b49e Mon Sep 17 00:00:00 2001 From: Kelly Guo Date: Thu, 18 Sep 2025 00:19:48 -0700 Subject: [PATCH 43/50] Updates github actions to error on doc warnings (#3488) # Description Some more infrastructure updates to brush up our automated jobs: - treat warnings as errors in doc building and fixing some existing warnings - adding release branches to the doc versions - making sure all jobs also get triggered on release branches - fixes make script on windows - fixes out of space error for license check job ## Type of change - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: ooctipus --- .github/workflows/docs.yaml | 4 ++-- .github/workflows/license-check.yaml | 20 ++++++++++------- .github/workflows/license-exceptions.json | 27 ++++++++++++++++++++++- .github/workflows/pre-commit.yaml | 3 +-- docs/Makefile | 3 ++- docs/conf.py | 2 +- docs/make.bat | 11 ++++----- docs/source/api/lab/isaaclab.assets.rst | 2 +- docs/source/api/lab/isaaclab.sensors.rst | 2 +- 9 files changed, 52 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 36ceeffa834..08bf3d2a8bf 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -10,6 +10,7 @@ on: branches: - main - devel + - 'release/**' pull_request: types: [opened, synchronize, reopened] @@ -27,8 +28,7 @@ jobs: - id: trigger-deploy env: REPO_NAME: ${{ secrets.REPO_NAME }} - BRANCH_REF: ${{ secrets.BRANCH_REF }} - if: "${{ github.repository == env.REPO_NAME && github.ref == env.BRANCH_REF }}" + if: "${{ github.repository == env.REPO_NAME && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/devel' || startsWith(github.ref, 'refs/heads/release/')) }}" run: echo "defined=true" >> "$GITHUB_OUTPUT" build-docs: diff --git a/.github/workflows/license-check.yaml b/.github/workflows/license-check.yaml index 3e7b190cbac..6260199e1dc 100644 --- a/.github/workflows/license-check.yaml +++ b/.github/workflows/license-check.yaml @@ -24,16 +24,20 @@ jobs: # - name: Install jq # run: sudo apt-get update && sudo apt-get install -y jq + - name: Clean up disk space + run: | + rm -rf /opt/hostedtoolcache + - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' # Adjust as needed + python-version: '3.11' # Adjust as needed - name: Install dependencies using ./isaaclab.sh -i run: | # first install isaac sim pip install --upgrade pip - pip install 'isaacsim[all,extscache]==4.5.0' --extra-index-url https://pypi.nvidia.com + pip install 'isaacsim[all,extscache]==${{ vars.ISAACSIM_BASE_VERSION || '5.0.0' }}' --extra-index-url https://pypi.nvidia.com chmod +x ./isaaclab.sh # Make sure the script is executable # install all lab dependencies ./isaaclab.sh -i @@ -48,6 +52,12 @@ jobs: - name: Print License Report run: pip-licenses --from=mixed --format=markdown + # Print pipdeptree + - name: Print pipdeptree + run: | + pip install pipdeptree + pipdeptree + - name: Check licenses against whitelist and exceptions run: | # Define the whitelist of allowed licenses @@ -118,9 +128,3 @@ jobs: else echo "All packages were checked." fi - - # Print pipdeptree - - name: Print pipdeptree - run: | - pip install pipdeptree - pipdeptree diff --git a/.github/workflows/license-exceptions.json b/.github/workflows/license-exceptions.json index 66530033efa..c243a16f776 100644 --- a/.github/workflows/license-exceptions.json +++ b/.github/workflows/license-exceptions.json @@ -308,7 +308,7 @@ }, { "package": "typing_extensions", - "license": "UNKNOWN", + "license": "Python Software Foundation License", "comment": "PSFL / OSRB" }, { @@ -400,5 +400,30 @@ "package": "fsspec", "license" : "UNKNOWN", "comment": "BSD" + }, + { + "package": "numpy-quaternion", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "aiohappyeyeballs", + "license": "Other/Proprietary License; Python Software Foundation License", + "comment": "PSFL / OSRB" + }, + { + "package": "cffi", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "trio", + "license": "UNKNOWN", + "comment": "MIT" + }, + { + "package": "pipdeptree", + "license": "UNKNOWN", + "comment": "MIT" } ] diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index f59d4ab7463..05e0d7d60af 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -7,8 +7,7 @@ name: Run linters using pre-commit on: pull_request: - push: - branches: [main] + types: [opened, synchronize, reopened] jobs: pre-commit: diff --git a/docs/Makefile b/docs/Makefile index ce33dad5033..0bff236671c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -15,4 +15,5 @@ multi-docs: .PHONY: current-docs current-docs: - @$(SPHINXBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/current" $(SPHINXOPTS) + @rm -rf "$(BUILDDIR)/current" + @$(SPHINXBUILD) -W --keep-going "$(SOURCEDIR)" "$(BUILDDIR)/current" $(SPHINXOPTS) diff --git a/docs/conf.py b/docs/conf.py index 7c68e911018..e63039a9c6e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -282,7 +282,7 @@ # Whitelist pattern for remotes smv_remote_whitelist = r"^.*$" # Whitelist pattern for branches (set to None to ignore all branches) -smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|devel)$") +smv_branch_whitelist = os.getenv("SMV_BRANCH_WHITELIST", r"^(main|devel|release/.*)$") # Whitelist pattern for tags (set to None to ignore all tags) smv_tag_whitelist = os.getenv("SMV_TAG_WHITELIST", r"^v[1-9]\d*\.\d+\.\d+$") html_sidebars = { diff --git a/docs/make.bat b/docs/make.bat index cdaf22f257c..941689ef03c 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -13,8 +13,8 @@ if "%1" == "multi-docs" ( if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-multiversion ) - %SPHINXBUILD% >NUL 2>NUL - if errorlevel 9009 ( + where %SPHINXBUILD% >NUL 2>NUL + if errorlevel 1 ( echo. echo.The 'sphinx-multiversion' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point @@ -37,8 +37,8 @@ if "%1" == "current-docs" ( if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) - %SPHINXBUILD% >NUL 2>NUL - if errorlevel 9009 ( + where %SPHINXBUILD% >NUL 2>NUL + if errorlevel 1 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point @@ -49,7 +49,8 @@ if "%1" == "current-docs" ( echo.http://sphinx-doc.org/ exit /b 1 ) - %SPHINXBUILD% %SOURCEDIR% %BUILDDIR%\current %SPHINXOPTS% %O% + if exist "%BUILDDIR%\current" rmdir /s /q "%BUILDDIR%\current" + %SPHINXBUILD% -W "%SOURCEDIR%" "%BUILDDIR%\current" %SPHINXOPTS% goto end ) diff --git a/docs/source/api/lab/isaaclab.assets.rst b/docs/source/api/lab/isaaclab.assets.rst index 338d729ddb6..c91066966e8 100644 --- a/docs/source/api/lab/isaaclab.assets.rst +++ b/docs/source/api/lab/isaaclab.assets.rst @@ -32,7 +32,7 @@ Asset Base .. autoclass:: AssetBaseCfg :members: - :exclude-members: __init__, class_type + :exclude-members: __init__, class_type, InitialStateCfg Rigid Object ------------ diff --git a/docs/source/api/lab/isaaclab.sensors.rst b/docs/source/api/lab/isaaclab.sensors.rst index 17ce71e3827..c30ed948f09 100644 --- a/docs/source/api/lab/isaaclab.sensors.rst +++ b/docs/source/api/lab/isaaclab.sensors.rst @@ -61,7 +61,7 @@ USD Camera :members: :inherited-members: :show-inheritance: - :exclude-members: __init__, class_type + :exclude-members: __init__, class_type, OffsetCfg Tile-Rendered USD Camera ------------------------ From e4d644f2b1ffccd184b909e8d65a83d86847a6ea Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:25:00 +0200 Subject: [PATCH 44/50] Abstracts out common steps in installation guide (#3445) # Description A lot of content is shared between different installation guides. This MR moves them to a common "include" so that it is easier for us to maintain and modify these changes. ## Type of change - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo --- docs/index.rst | 3 +- docs/source/deployment/index.rst | 68 ++- .../setup/installation/asset_caching.rst | 2 +- .../installation/binaries_installation.rst | 493 +----------------- .../setup/installation/cloud_installation.rst | 149 ++++-- .../include/bin_verify_isaacsim.rst | 74 +++ .../include/pip_python_virtual_env.rst | 123 +++++ .../include/pip_verify_isaacsim.rst | 46 ++ .../include/src_build_isaaclab.rst | 56 ++ .../include/src_clone_isaaclab.rst | 78 +++ .../include/src_python_virtual_env.rst | 112 ++++ .../include/src_symlink_isaacsim.rst | 43 ++ .../include/src_verify_isaaclab.rst | 101 ++++ docs/source/setup/installation/index.rst | 41 +- .../isaaclab_pip_installation.rst | 165 +----- .../setup/installation/pip_installation.rst | 406 +-------------- .../installation/source_installation.rst | 456 +--------------- 17 files changed, 918 insertions(+), 1498 deletions(-) create mode 100644 docs/source/setup/installation/include/bin_verify_isaacsim.rst create mode 100644 docs/source/setup/installation/include/pip_python_virtual_env.rst create mode 100644 docs/source/setup/installation/include/pip_verify_isaacsim.rst create mode 100644 docs/source/setup/installation/include/src_build_isaaclab.rst create mode 100644 docs/source/setup/installation/include/src_clone_isaaclab.rst create mode 100644 docs/source/setup/installation/include/src_python_virtual_env.rst create mode 100644 docs/source/setup/installation/include/src_symlink_isaacsim.rst create mode 100644 docs/source/setup/installation/include/src_verify_isaaclab.rst diff --git a/docs/index.rst b/docs/index.rst index 7d1e4cbc8c8..fbffccd6820 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -74,12 +74,13 @@ Table of Contents ================= .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Isaac Lab source/setup/ecosystem source/setup/installation/index source/deployment/index + source/setup/installation/cloud_installation source/refs/reference_architecture/index diff --git a/docs/source/deployment/index.rst b/docs/source/deployment/index.rst index a7791a395e6..235a23c9d75 100644 --- a/docs/source/deployment/index.rst +++ b/docs/source/deployment/index.rst @@ -1,3 +1,5 @@ +.. _container-deployment: + Container Deployment ==================== @@ -11,13 +13,65 @@ The Dockerfile is based on the Isaac Sim image provided by NVIDIA, which include application launcher and the Isaac Sim application. The Dockerfile installs Isaac Lab and its dependencies on top of this image. -The following guides provide instructions for building the Docker image and running Isaac Lab in a -container. +Cloning the Repository +---------------------- + +Before building the container, clone the Isaac Lab repository (if not already done): + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:isaac-sim/IsaacLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacLab.git + +Next Steps +---------- + +After cloning, you can choose the deployment workflow that fits your needs: + +- :doc:`docker` + + - Learn how to build, configure, and run Isaac Lab in Docker containers. + - Explains the repository's ``docker/`` setup, the ``container.py`` helper script, mounted volumes, + image extensions (like ROS 2), and optional CloudXR streaming support. + - Covers running pre-built Isaac Lab containers from NVIDIA NGC for headless training. + +- :doc:`run_docker_example` + + - Learn how to run a development workflow inside the Isaac Lab Docker container. + - Demonstrates building the container, entering it, executing a sample Python script (`log_time.py`), + and retrieving logs using mounted volumes. + - Highlights bind-mounted directories for live code editing and explains how to stop or remove the container + while keeping the image and artifacts. + +- :doc:`cluster` + + - Learn how to run Isaac Lab on high-performance computing (HPC) clusters. + - Explains how to export the Docker image to a Singularity (Apptainer) image, configure cluster-specific parameters, + and submit jobs using common workload managers (SLURM or PBS). + - Includes tested workflows for ETH Zurich's Euler cluster and IIT Genoa's Franklin cluster, + with notes on adapting to other environments. + +- :doc:`cloudxr_teleoperation_cluster` + + - Deploy CloudXR Teleoperation for Isaac Lab on a Kubernetes cluster. + - Covers system requirements, software dependencies, and preparation steps including RBAC permissions. + - Demonstrates how to install and verify the Helm chart, run the pod, and uninstall it. + .. toctree:: - :maxdepth: 1 + :maxdepth: 1 + :hidden: - docker - cluster - cloudxr_teleoperation_cluster - run_docker_example + docker + run_docker_example + cluster + cloudxr_teleoperation_cluster diff --git a/docs/source/setup/installation/asset_caching.rst b/docs/source/setup/installation/asset_caching.rst index 4c67729bb6a..5cee207fae3 100644 --- a/docs/source/setup/installation/asset_caching.rst +++ b/docs/source/setup/installation/asset_caching.rst @@ -34,7 +34,7 @@ Click the message to enable `Hub `__ -to install the latest Isaac Sim release. +Isaac Sim binaries can be downloaded directly as a zip file from +`here `__. +If you wish to use the older Isaac Sim 4.5 release, please check the older download page +`here `__. -From Isaac Sim 4.5 release, Isaac Sim binaries can be `downloaded `_ -directly as a zip file. - -To check the minimum system requirements, refer to the documentation -`here `__. +Once the zip file is downloaded, you can unzip it to the desired directory. +As an example set of instructions for unzipping the Isaac Sim binaries, +please refer to the `Isaac Sim documentation `__. .. tab-set:: :sync-group: os @@ -27,23 +26,12 @@ To check the minimum system requirements, refer to the documentation .. tab-item:: :icon:`fa-brands fa-linux` Linux :sync: linux - .. note:: - - For details on driver requirements, please see the `Technical Requirements `_ guide! - - On Linux systems, Isaac Sim directory will be named ``${HOME}/isaacsim``. + On Linux systems, we assume the Isaac Sim directory is named ``${HOME}/isaacsim``. .. tab-item:: :icon:`fa-brands fa-windows` Windows :sync: windows - .. note:: - - For details on driver requirements, please see the `Technical Requirements `_ guide! - - From Isaac Sim 4.5 release, Isaac Sim binaries can be downloaded directly as a zip file. - The below steps assume the Isaac Sim folder was unzipped to the ``C:/isaacsim`` directory. - - On Windows systems, Isaac Sim directory will be named ``C:/isaacsim``. + On Windows systems, we assume the Isaac Sim directory is named ``C:\isaacsim``. Verifying the Isaac Sim installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -71,465 +59,22 @@ variables to your terminal for the remaining of the installation instructions: .. code:: batch :: Isaac Sim root directory - set ISAACSIM_PATH="C:/isaacsim" + set ISAACSIM_PATH="C:\isaacsim" :: Isaac Sim python executable set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" -For more information on common paths, please check the Isaac Sim -`documentation `__. - - -- Check that the simulator runs as expected: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # note: you can pass the argument "--help" to see all arguments possible. - ${ISAACSIM_PATH}/isaac-sim.sh - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: note: you can pass the argument "--help" to see all arguments possible. - %ISAACSIM_PATH%\isaac-sim.bat - - -- Check that the simulator runs from a standalone python script: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # checks that python path is set correctly - ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')" - # checks that Isaac Sim can be launched from python - ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.api/add_cubes.py - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: checks that python path is set correctly - %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')" - :: checks that Isaac Sim can be launched from python - %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.api\add_cubes.py - - -.. caution:: - - If you have been using a previous version of Isaac Sim, you need to run the following command for the *first* - time after installation to remove all the old user data and cached variables: - - .. tab-set:: - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - - .. code:: bash - - ${ISAACSIM_PATH}/isaac-sim.sh --reset-user - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - - .. code:: batch - - %ISAACSIM_PATH%\isaac-sim.bat --reset-user - - -If the simulator does not run or crashes while following the above -instructions, it means that something is incorrectly configured. To -debug and troubleshoot, please check Isaac Sim -`documentation `__ -and the -`forums `__. - +.. include:: include/bin_verify_isaacsim.rst Installing Isaac Lab -------------------- -Cloning Isaac Lab -~~~~~~~~~~~~~~~~~ - -.. note:: - - We recommend making a `fork `_ of the Isaac Lab repository to contribute - to the project but this is not mandatory to use the framework. If you - make a fork, please replace ``isaac-sim`` with your username - in the following instructions. - -Clone the Isaac Lab repository into your workspace: - -.. tab-set:: - - .. tab-item:: SSH - - .. code:: bash - - git clone git@github.com:isaac-sim/IsaacLab.git - - .. tab-item:: HTTPS - - .. code:: bash - - git clone https://github.com/isaac-sim/IsaacLab.git - - -.. note:: - We provide a helper executable `isaaclab.sh `_ that provides - utilities to manage extensions: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: text - - ./isaaclab.sh --help - - usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl-games, rsl-rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -o, --docker Run the docker container helper script (docker/container.sh). - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. - -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: text - - isaaclab.bat --help - - usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl-games, rsl-rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. - -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. - - -Creating the Isaac Sim Symbolic Link -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Set up a symbolic link between the installed Isaac Sim root folder -and ``_isaac_sim`` in the Isaac Lab directory. This makes it convenient -to index the python modules and look for extensions shipped with Isaac Sim. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # enter the cloned repository - cd IsaacLab - # create a symbolic link - ln -s path_to_isaac_sim _isaac_sim - # For example: ln -s ${HOME}/isaacsim _isaac_sim - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: enter the cloned repository - cd IsaacLab - :: create a symbolic link - requires launching Command Prompt with Administrator access - mklink /D _isaac_sim path_to_isaac_sim - :: For example: mklink /D _isaac_sim C:/isaacsim - -Setting up the uv environment (optional) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. attention:: - This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. - -The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac -Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable -behaves like a python executable, and can be used to run any python script or -module with the simulator. For more information, please refer to the -`documentation `__. - -To install ``uv``, please follow the instructions `here `__. -You can create the Isaac Lab environment using the following commands. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Default name for uv environment is 'env_isaaclab' - ./isaaclab.sh --uv # or "./isaaclab.sh -u" - # Option 2: Custom name for uv environment - ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Default name for uv environment is 'env_isaaclab' - isaaclab.bat --uv :: or "isaaclab.bat -u" - :: Option 2: Custom name for uv environment - isaaclab.bat --uv my_env :: or "isaaclab.bat -u my_env" - - -Once created, be sure to activate the environment before proceeding! - -.. code:: bash - - source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" - -Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` -to run python scripts. You can use the default python executable in your environment -by running ``python`` or ``python3``. However, for the rest of the documentation, -we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command -is equivalent to running ``python`` or ``python3`` in your virtual environment. - - -Setting up the conda environment (optional) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. attention:: - This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. - -.. note:: - - If you use Conda, we recommend using `Miniconda `_. - -The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac -Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable -behaves like a python executable, and can be used to run any python script or -module with the simulator. For more information, please refer to the -`documentation `__. +.. include:: include/src_clone_isaaclab.rst -To install ``conda``, please follow the instructions `here `__. -You can create the Isaac Lab environment using the following commands. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Default name for conda environment is 'env_isaaclab' - ./isaaclab.sh --conda # or "./isaaclab.sh -c" - # Option 2: Custom name for conda environment - ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Default name for conda environment is 'env_isaaclab' - isaaclab.bat --conda :: or "isaaclab.bat -c" - :: Option 2: Custom name for conda environment - isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" - - -Once created, be sure to activate the environment before proceeding! - -.. code:: bash - - conda activate env_isaaclab # or "conda activate my_env" - -Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` -to run python scripts. You can use the default python executable in your environment -by running ``python`` or ``python3``. However, for the rest of the documentation, -we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command -is equivalent to running ``python`` or ``python3`` in your virtual environment. - - -Installation -~~~~~~~~~~~~ - -- Install dependencies using ``apt`` (on Linux only): - - .. code:: bash - - # these dependency are needed by robomimic which is not available on Windows - sudo apt install cmake build-essential - -- Run the install command that iterates over all the extensions in ``source`` directory and installs them - using pip (with ``--editable`` flag): - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh --install # or "./isaaclab.sh -i" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - isaaclab.bat --install :: or "isaaclab.bat -i" - -.. note:: - - By default, the above will install all the learning frameworks. If you want to install only a specific framework, you can - pass the name of the framework as an argument. For example, to install only the ``rl_games`` framework, you can run - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" - - The valid options are ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, ``none``. - - -Verifying the Isaac Lab installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To verify that the installation was successful, run the following command from the -top of the repository: - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Using the isaaclab.sh executable - # note: this works for both the bundled python and the virtual environment - ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py - - # Option 2: Using python in your virtual environment - python scripts/tutorials/00_sim/create_empty.py - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Using the isaaclab.bat executable - :: note: this works for both the bundled python and the virtual environment - isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py - - :: Option 2: Using python in your virtual environment - python scripts\tutorials\00_sim\create_empty.py - - -The above command should launch the simulator and display a window with a black -viewport. You can exit the script by pressing ``Ctrl+C`` on your terminal. -On Windows machines, please terminate the process from Command Prompt using -``Ctrl+Break`` or ``Ctrl+fn+B``. - -.. figure:: ../../_static/setup/verify_install.jpg - :align: center - :figwidth: 100% - :alt: Simulator with a black window. - - -If you see this, then the installation was successful! |:tada:| - -If you see an error ``ModuleNotFoundError: No module named 'isaacsim'``, ensure that the conda or uv environment is activated -and ``source _isaac_sim/setup_conda_env.sh`` has been executed (for uv as well). - - -Train a robot! -~~~~~~~~~~~~~~ - -You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! -We recommend adding ``--headless`` for faster training. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless - -... Or a robot dog! - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch +.. include:: include/src_symlink_isaacsim.rst - isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless +.. include:: include/src_python_virtual_env.rst -Isaac Lab provides the tools you'll need to create your own **Tasks** and **Workflows** for whatever your project needs may be. Take a look at our :ref:`how-to` guides like `Adding your own learning Library `_ or `Wrapping Environments `_ for details. +.. include:: include/src_build_isaaclab.rst -.. figure:: ../../_static/setup/isaac_ants_example.jpg - :align: center - :figwidth: 100% - :alt: Idle hands... +.. include:: include/src_verify_isaaclab.rst diff --git a/docs/source/setup/installation/cloud_installation.rst b/docs/source/setup/installation/cloud_installation.rst index 1f48a64b871..2b7034a1c04 100644 --- a/docs/source/setup/installation/cloud_installation.rst +++ b/docs/source/setup/installation/cloud_installation.rst @@ -1,30 +1,54 @@ -Running Isaac Lab in the Cloud -============================== +Cloud Deployment +================ -Isaac Lab can be run in various cloud infrastructures with the use of `Isaac Automator `__. -Isaac Automator allows for quick deployment of Isaac Sim and Isaac Lab onto the public clouds (AWS, GCP, Azure, and Alibaba Cloud are currently supported). +Isaac Lab can be run in various cloud infrastructures with the use of +`Isaac Automator `__. -The result is a fully configured remote desktop cloud workstation, which can be used for development and testing of Isaac Lab within minutes and on a budget. Isaac Automator supports variety of GPU instances and stop-start functionality to save on cloud costs and a variety of tools to aid the workflow (like uploading and downloading data, autorun, deployment management, etc). +Isaac Automator allows for quick deployment of Isaac Sim and Isaac Lab onto +the public clouds (AWS, GCP, Azure, and Alibaba Cloud are currently supported). +The result is a fully configured remote desktop cloud workstation, which can +be used for development and testing of Isaac Lab within minutes and on a budget. +Isaac Automator supports variety of GPU instances and stop-start functionality +to save on cloud costs and a variety of tools to aid the workflow +(such as uploading and downloading data, autorun, deployment management, etc). + + +System Requirements +------------------- + +Isaac Automator requires having ``docker`` pre-installed on the system. + +* To install Docker, please follow the instructions for your operating system on the + `Docker website`_. A minimum version of 26.0.0 for Docker Engine and 2.25.0 for Docker + compose are required to work with Isaac Automator. +* Follow the post-installation steps for Docker on the `post-installation steps`_ page. + These steps allow you to run Docker without using ``sudo``. Installing Isaac Automator -------------------------- -For the most update-to-date and complete installation instructions, please refer to `Isaac Automator `__. +For the most update-to-date and complete installation instructions, please refer to +`Isaac Automator `__. To use Isaac Automator, first clone the repo: -.. code-block:: bash +.. tab-set:: - git clone https://github.com/isaac-sim/IsaacAutomator.git + .. tab-item:: HTTPS -Isaac Automator requires having ``docker`` pre-installed on the system. + .. code-block:: bash -* To install Docker, please follow the instructions for your operating system on the `Docker website`_. A minimum version of 26.0.0 for Docker Engine and 2.25.0 for Docker compose are required to work with Isaac Automator. -* Follow the post-installation steps for Docker on the `post-installation steps`_ page. These steps allow you to run - Docker without using ``sudo``. + git clone https://github.com/isaac-sim/IsaacAutomator.git -Isaac Automator also requires obtaining a NGC API key. + .. tab-item:: SSH + + .. code-block:: bash + + git clone git@github.com:isaac-sim/IsaacAutomator.git + + +Isaac Automator requires obtaining a NGC API key. * Get access to the `Isaac Sim container`_ by joining the NVIDIA Developer Program credentials. * Generate your `NGC API key`_ to access locked container images from NVIDIA GPU Cloud (NGC). @@ -46,8 +70,8 @@ Isaac Automator also requires obtaining a NGC API key. Password: -Running Isaac Automator ------------------------ +Building the container +---------------------- To run Isaac Automator, first build the Isaac Automator container: @@ -68,7 +92,14 @@ To run Isaac Automator, first build the Isaac Automator container: docker build --platform linux/x86_64 -t isa . -Next, enter the automator container: + +This will build the Isaac Automator container and tag it as ``isa``. + + +Running the Automator Commands +------------------------------ + +First, enter the Automator container: .. tab-set:: :sync-group: os @@ -87,22 +118,54 @@ Next, enter the automator container: docker run --platform linux/x86_64 -it --rm -v .:/app isa bash -Next, run the deployed script for your preferred cloud: +Next, run the deployment script for your preferred cloud: + +.. note:: + + The ``--isaaclab`` flag is used to specify the version of Isaac Lab to deploy. + The ``v2.2.1`` tag is the latest release of Isaac Lab. + +.. tab-set:: + :sync-group: cloud + + .. tab-item:: AWS + :sync: aws + + .. code-block:: bash + + ./deploy-aws --isaaclab v2.2.1 + + .. tab-item:: Azure + :sync: azure + + .. code-block:: bash -.. code-block:: bash + ./deploy-azure --isaaclab v2.2.1 - # AWS - ./deploy-aws - # Azure - ./deploy-azure - # GCP - ./deploy-gcp - # Alibaba Cloud - ./deploy-alicloud + .. tab-item:: GCP + :sync: gcp + + .. code-block:: bash + + ./deploy-gcp --isaaclab v2.2.1 + + .. tab-item:: Alibaba Cloud + :sync: alicloud + + .. code-block:: bash + + ./deploy-alicloud --isaaclab v2.2.1 Follow the prompts for entering information regarding the environment setup and credentials. -Once successful, instructions for connecting to the cloud instance will be available in the terminal. -Connections can be made using SSH, noVCN, or NoMachine. +Once successful, instructions for connecting to the cloud instance will be available +in the terminal. The deployed Isaac Sim instances can be accessed via: + +- SSH +- noVCN (browser-based VNC client) +- NoMachine (remote desktop client) + +Look for the connection instructions at the end of the deployment command output. +Additionally, this info is saved in ``state//info.txt`` file. For details on the credentials and setup required for each cloud, please visit the `Isaac Automator `__ @@ -133,16 +196,36 @@ For example: .. code-block:: batch - ./isaaclab.bat -p scripts/reinforcement_learning/rl_games/train.py --task=Isaac-Cartpole-v0 + isaaclab.bat -p scripts/reinforcement_learning/rl_games/train.py --task=Isaac-Cartpole-v0 -Destroying a Development -------------------------- +Destroying a Deployment +----------------------- To save costs, deployments can be destroyed when not being used. -This can be done from within the Automator container, which can be entered with command ``./run``. +This can be done from within the Automator container. + +Enter the Automator container with the command described in the previous section: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + ./run + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + docker run --platform linux/x86_64 -it --rm -v .:/app isa bash + -To destroy a deployment, run: +To destroy a deployment, run the following command from within the container: .. code:: bash diff --git a/docs/source/setup/installation/include/bin_verify_isaacsim.rst b/docs/source/setup/installation/include/bin_verify_isaacsim.rst new file mode 100644 index 00000000000..4457255625f --- /dev/null +++ b/docs/source/setup/installation/include/bin_verify_isaacsim.rst @@ -0,0 +1,74 @@ +Check that the simulator runs as expected: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + ${ISAACSIM_PATH}/isaac-sim.sh + + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: note: you can pass the argument "--help" to see all arguments possible. + %ISAACSIM_PATH%\isaac-sim.bat + + +Check that the simulator runs from a standalone python script: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # checks that python path is set correctly + ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')" + # checks that Isaac Sim can be launched from python + ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.api/add_cubes.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: checks that python path is set correctly + %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')" + :: checks that Isaac Sim can be launched from python + %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.api\add_cubes.py + +.. caution:: + + If you have been using a previous version of Isaac Sim, you need to run the following command for the *first* + time after installation to remove all the old user data and cached variables: + + .. tab-set:: + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + + .. code:: bash + + ${ISAACSIM_PATH}/isaac-sim.sh --reset-user + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + + .. code:: batch + + %ISAACSIM_PATH%\isaac-sim.bat --reset-user + + +If the simulator does not run or crashes while following the above +instructions, it means that something is incorrectly configured. To +debug and troubleshoot, please check Isaac Sim +`documentation `__ +and the +`forums `__. diff --git a/docs/source/setup/installation/include/pip_python_virtual_env.rst b/docs/source/setup/installation/include/pip_python_virtual_env.rst new file mode 100644 index 00000000000..e7ff0872190 --- /dev/null +++ b/docs/source/setup/installation/include/pip_python_virtual_env.rst @@ -0,0 +1,123 @@ +Preparing a Python Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Creating a dedicated Python environment is **strongly recommended**. It helps: + +- **Avoid conflicts with system Python** or other projects installed on your machine. +- **Keep dependencies isolated**, so that package upgrades or experiments in other projects + do not break Isaac Sim. +- **Easily manage multiple environments** for setups with different versions of dependencies. +- **Simplify reproducibility** — the environment contains only the packages needed for the current project, + making it easier to share setups with colleagues or run on different machines. + +You can choose different package managers to create a virtual environment. + +- **UV**: A modern, fast, and secure package manager for Python. +- **Conda**: A cross-platform, language-agnostic package manager for Python. +- **venv**: The standard library for creating virtual environments in Python. + +.. caution:: + + The Python version of the virtual environment must match the Python version of Isaac Sim. + + - For Isaac Sim 5.X, the required Python version is 3.11. + - For Isaac Sim 4.X, the required Python version is 3.10. + + Using a different Python version will result in errors when running Isaac Lab. + +The following instructions are for Isaac Sim 5.X, which requires Python 3.11. +If you wish to install Isaac Sim 4.5, please use modify the instructions accordingly to use Python 3.10. + +- Create a virtual environment using one of the package managers: + + .. tab-set:: + + .. tab-item:: UV Environment + + To install ``uv``, please follow the instructions `here `__. + You can create the Isaac Lab environment using the following commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + # activate the virtual environment + source env_isaaclab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + :: create a virtual environment named env_isaaclab with python3.11 + uv venv --python 3.11 env_isaaclab + :: activate the virtual environment + env_isaaclab\Scripts\activate + + .. tab-item:: Conda Environment + + To install conda, please follow the instructions `here __`. + You can create the Isaac Lab environment using the following commands. + + We recommend using `Miniconda `_, + since it is light-weight and resource-efficient environment management system. + + .. code-block:: bash + + conda create -n env_isaaclab python=3.11 + conda activate env_isaaclab + + .. tab-item:: venv Environment + + To create a virtual environment using the standard library, you can use the + following commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named env_isaaclab with python3.11 + python3.11 -m venv env_isaaclab + # activate the virtual environment + source env_isaaclab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + :: create a virtual environment named env_isaaclab with python3.11 + python3.11 -m venv env_isaaclab + :: activate the virtual environment + env_isaaclab\Scripts\activate + + +- Ensure the latest pip version is installed. To update pip, run the following command + from inside the virtual environment: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + pip install --upgrade pip + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + python -m pip install --upgrade pip diff --git a/docs/source/setup/installation/include/pip_verify_isaacsim.rst b/docs/source/setup/installation/include/pip_verify_isaacsim.rst new file mode 100644 index 00000000000..2b63bb1017c --- /dev/null +++ b/docs/source/setup/installation/include/pip_verify_isaacsim.rst @@ -0,0 +1,46 @@ + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Make sure that your virtual environment is activated (if applicable) + +- Check that the simulator runs as expected: + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + isaacsim + +- It's also possible to run with a specific experience file, run: + + .. code:: bash + + # experience files can be absolute path, or relative path searched in isaacsim/apps or omni/apps + isaacsim isaacsim.exp.full.kit + + +.. note:: + + When running Isaac Sim for the first time, all dependent extensions will be pulled from the registry. + This process can take upwards of 10 minutes and is required on the first run of each experience file. + Once the extensions are pulled, consecutive runs using the same experience file will use the cached extensions. + +.. attention:: + + The first run will prompt users to accept the Nvidia Omniverse License Agreement. + To accept the EULA, reply ``Yes`` when prompted with the below message: + + .. code:: bash + + By installing or using Isaac Sim, I agree to the terms of NVIDIA OMNIVERSE LICENSE AGREEMENT (EULA) + in https://docs.isaacsim.omniverse.nvidia.com/latest/common/NVIDIA_Omniverse_License_Agreement.html + + Do you accept the EULA? (Yes/No): Yes + + +If the simulator does not run or crashes while following the above +instructions, it means that something is incorrectly configured. To +debug and troubleshoot, please check Isaac Sim +`documentation `__ +and the +`forums `__. diff --git a/docs/source/setup/installation/include/src_build_isaaclab.rst b/docs/source/setup/installation/include/src_build_isaaclab.rst new file mode 100644 index 00000000000..ba822ae7b2c --- /dev/null +++ b/docs/source/setup/installation/include/src_build_isaaclab.rst @@ -0,0 +1,56 @@ +Installation +~~~~~~~~~~~~ + +- Install dependencies using ``apt`` (on Linux only): + + .. code:: bash + + # these dependency are needed by robomimic which is not available on Windows + sudo apt install cmake build-essential + +- Run the install command that iterates over all the extensions in ``source`` directory and installs them + using pip (with ``--editable`` flag): + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install # or "./isaaclab.sh -i" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat --install :: or "isaaclab.bat -i" + + + By default, the above will install **all** the learning frameworks. These include + ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``. + + If you want to install only a specific framework, you can pass the name of the framework + as an argument. For example, to install only the ``rl_games`` framework, you can run: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" + + The valid options are ``all``, ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, + and ``none``. If ``none`` is passed, then no learning frameworks will be installed. diff --git a/docs/source/setup/installation/include/src_clone_isaaclab.rst b/docs/source/setup/installation/include/src_clone_isaaclab.rst new file mode 100644 index 00000000000..844cac2f3fd --- /dev/null +++ b/docs/source/setup/installation/include/src_clone_isaaclab.rst @@ -0,0 +1,78 @@ +Cloning Isaac Lab +~~~~~~~~~~~~~~~~~ + +.. note:: + + We recommend making a `fork `_ of the Isaac Lab repository to contribute + to the project but this is not mandatory to use the framework. If you + make a fork, please replace ``isaac-sim`` with your username + in the following instructions. + +Clone the Isaac Lab repository into your project's workspace: + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:isaac-sim/IsaacLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacLab.git + + +We provide a helper executable `isaaclab.sh `_ +and `isaaclab.bat `_ for Linux and Windows +respectively that provides utilities to manage extensions. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: text + + ./isaaclab.sh --help + + usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. + -t, --test Run all python pytest tests. + -o, --docker Run the docker container helper script (docker/container.sh). + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -n, --new Create a new external project or internal task from template. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: text + + isaaclab.bat --help + + usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. + -t, --test Run all python pytest tests. + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -n, --new Create a new external project or internal task from template. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. + -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. diff --git a/docs/source/setup/installation/include/src_python_virtual_env.rst b/docs/source/setup/installation/include/src_python_virtual_env.rst new file mode 100644 index 00000000000..7757e40ca31 --- /dev/null +++ b/docs/source/setup/installation/include/src_python_virtual_env.rst @@ -0,0 +1,112 @@ +Setting up a Python Environment (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attention:: + This step is optional. If you are using the bundled Python with Isaac Sim, you can skip this step. + +Creating a dedicated Python environment for Isaac Lab is **strongly recommended**, even though +it is optional. Using a virtual environment helps: + +- **Avoid conflicts with system Python** or other projects installed on your machine. +- **Keep dependencies isolated**, so that package upgrades or experiments in other projects + do not break Isaac Sim. +- **Easily manage multiple environments** for setups with different versions of dependencies. +- **Simplify reproducibility** — the environment contains only the packages needed for the current project, + making it easier to share setups with colleagues or run on different machines. + + +You can choose different package managers to create a virtual environment. + +- **UV**: A modern, fast, and secure package manager for Python. +- **Conda**: A cross-platform, language-agnostic package manager for Python. + +Once created, you can use the default Python in the virtual environment (*python* or *python3*) +instead of *./isaaclab.sh -p* or *isaaclab.bat -p*. + +.. caution:: + + The Python version of the virtual environment must match the Python version of Isaac Sim. + + - For Isaac Sim 5.X, the required Python version is 3.11. + - For Isaac Sim 4.X, the required Python version is 3.10. + + Using a different Python version will result in errors when running Isaac Lab. + + +.. tab-set:: + + .. tab-item:: UV Environment + + To install ``uv``, please follow the instructions `here `__. + You can create the Isaac Lab environment using the following commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default environment name 'env_isaaclab' + ./isaaclab.sh --uv # or "./isaaclab.sh -u" + # Option 2: Custom name + ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" + + .. code:: bash + + # Activate environment + source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. warning:: + Windows support for UV is currently unavailable. Please check + `issue #3483 `_ to track progress. + + .. tab-item:: Conda Environment + + To install conda, please follow the instructions `here __`. + You can create the Isaac Lab environment using the following commands. + + We recommend using `Miniconda `_, + since it is light-weight and resource-efficient environment management system. + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default environment name 'env_isaaclab' + ./isaaclab.sh --conda # or "./isaaclab.sh -c" + # Option 2: Custom name + ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" + + .. code:: bash + + # Activate environment + conda activate env_isaaclab # or "conda activate my_env" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Default environment name 'env_isaaclab' + isaaclab.bat --conda :: or "isaaclab.bat -c" + :: Option 2: Custom name + isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" + + .. code:: batch + + :: Activate environment + conda activate env_isaaclab # or "conda activate my_env" + +Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` or +``isaaclab.bat -p`` to run python scripts. You can use the default python executable in your +environment by running ``python`` or ``python3``. However, for the rest of the documentation, +we will assume that you are using ``./isaaclab.sh -p`` or ``isaaclab.bat -p`` to run python scripts. diff --git a/docs/source/setup/installation/include/src_symlink_isaacsim.rst b/docs/source/setup/installation/include/src_symlink_isaacsim.rst new file mode 100644 index 00000000000..be8ae17cdbd --- /dev/null +++ b/docs/source/setup/installation/include/src_symlink_isaacsim.rst @@ -0,0 +1,43 @@ +Creating the Isaac Sim Symbolic Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set up a symbolic link between the installed Isaac Sim root folder +and ``_isaac_sim`` in the Isaac Lab directory. This makes it convenient +to index the python modules and look for extensions shipped with Isaac Sim. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # enter the cloned repository + cd IsaacLab + # create a symbolic link + ln -s ${ISAACSIM_PATH} _isaac_sim + + # For example: + # Option 1: If pre-built binaries were installed: + # ln -s ${HOME}/isaacsim _isaac_sim + # + # Option 2: If Isaac Sim was built from source: + # ln -s ${HOME}/IsaacSim/_build/linux-x86_64/release _isaac_sim + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: enter the cloned repository + cd IsaacLab + :: create a symbolic link - requires launching Command Prompt with Administrator access + mklink /D _isaac_sim %ISAACSIM_PATH% + + :: For example: + :: Option 1: If pre-built binaries were installed: + :: mklink /D _isaac_sim C:\isaacsim + :: + :: Option 2: If Isaac Sim was built from source: + :: mklink /D _isaac_sim C:\IsaacSim\_build\windows-x86_64\release diff --git a/docs/source/setup/installation/include/src_verify_isaaclab.rst b/docs/source/setup/installation/include/src_verify_isaaclab.rst new file mode 100644 index 00000000000..020b961f3b8 --- /dev/null +++ b/docs/source/setup/installation/include/src_verify_isaaclab.rst @@ -0,0 +1,101 @@ +Verifying the Isaac Lab installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To verify that the installation was successful, run the following command from the +top of the repository: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Using the isaaclab.sh executable + # note: this works for both the bundled python and the virtual environment + ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py + + # Option 2: Using python in your virtual environment + python scripts/tutorials/00_sim/create_empty.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Using the isaaclab.bat executable + :: note: this works for both the bundled python and the virtual environment + isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py + + :: Option 2: Using python in your virtual environment + python scripts\tutorials\00_sim\create_empty.py + + +The above command should launch the simulator and display a window with a black +viewport. You can exit the script by pressing ``Ctrl+C`` on your terminal. +On Windows machines, please terminate the process from Command Prompt using +``Ctrl+Break`` or ``Ctrl+fn+B``. + +.. figure:: /source/_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + +If you see this, then the installation was successful! |:tada:| + +.. note:: + + If you see an error ``ModuleNotFoundError: No module named 'isaacsim'``, please ensure that the virtual + environment is activated and ``source _isaac_sim/setup_conda_env.sh`` has been executed (for uv as well). + + +Train a robot! +~~~~~~~~~~~~~~ + +You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! +We recommend adding ``--headless`` for faster training. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless + +... Or a robot dog! + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless + +Isaac Lab provides the tools you'll need to create your own **Tasks** and **Workflows** for whatever your project needs may be. Take a look at our :ref:`how-to` guides like `Adding your own learning Library `_ or `Wrapping Environments `_ for details. + +.. figure:: /source/_static/setup/isaac_ants_example.jpg + :align: center + :figwidth: 100% + :alt: Idle hands... diff --git a/docs/source/setup/installation/index.rst b/docs/source/setup/installation/index.rst index 77cc994b3a1..d16ad40d75a 100644 --- a/docs/source/setup/installation/index.rst +++ b/docs/source/setup/installation/index.rst @@ -39,12 +39,21 @@ System Requirements General Requirements ~~~~~~~~~~~~~~~~~~~~ +For detailed requirements, please see the +`Isaac Sim system requirements `_. +The basic requirements are: + +- **OS:** Ubuntu 22.04 (Linux x64) or Windows 11 (x64) - **RAM:** 32 GB or more - **GPU VRAM:** 16 GB or more (additional VRAM may be required for rendering workflows) -- **OS:** Ubuntu 22.04 (Linux x64) or Windows 11 (x64) -For detailed requirements, see the -`Isaac Sim system requirements `_. +**Isaac Sim is built against a specific Python version**, making +it essential to use the same Python version when installing Isaac Lab. +The required Python version is as follows: + +- For Isaac Sim 5.X, the required Python version is 3.11. +- For Isaac Sim 4.X, the required Python version is 3.10. + Driver Requirements ~~~~~~~~~~~~~~~~~~~ @@ -65,6 +74,8 @@ Troubleshooting Please refer to the `Linux Troubleshooting `_ to resolve installation issues in Linux. +You can use `Isaac Sim Compatibility Checker `_ +to automatically check if the above requirements are met for running Isaac Sim on your system. Quick Start (Recommended) ------------------------- @@ -95,7 +106,8 @@ Use this table to decide: | Pip Only | |:package:| pip install | |:package:| pip install | External extensions only | Special | | | | | (no training/examples) | case | +-------------------+------------------------------+------------------------------+---------------------------+------------+ - +| Docker | |:whale:| Docker | |:floppy_disk:| source (git) | Docker users | Advanced | ++-------------------+------------------------------+------------------------------+---------------------------+------------+ Next Steps ---------- @@ -125,6 +137,11 @@ Once you've reviewed the installation methods, continue with the guide that matc - Best for advanced users building **external extensions** with custom runner scripts. - Note: This does **not** include training or example scripts. +- :ref:`container-deployment` + + - Install Isaac Sim and Isaac Lab in a Docker container. + - Best for users who want to use Isaac Lab in a containerized environment. + Asset Caching ------------- @@ -142,11 +159,11 @@ Please follow the steps :doc:`asset_caching` to enable asset caching and speed u .. toctree:: - :maxdepth: 2 - :hidden: - - pip_installation - binaries_installation - source_installation - isaaclab_pip_installation - asset_caching + :maxdepth: 1 + :hidden: + + pip_installation + binaries_installation + source_installation + isaaclab_pip_installation + asset_caching diff --git a/docs/source/setup/installation/isaaclab_pip_installation.rst b/docs/source/setup/installation/isaaclab_pip_installation.rst index d9d9441ca2f..b1d85d173bf 100644 --- a/docs/source/setup/installation/isaaclab_pip_installation.rst +++ b/docs/source/setup/installation/isaaclab_pip_installation.rst @@ -1,5 +1,5 @@ -Installing Isaac Lab through Pip -================================ +Installation using Isaac Lab Pip Packages +========================================= From Isaac Lab 2.0, pip packages are provided to install both Isaac Sim and Isaac Lab extensions from pip. Note that this installation process is only recommended for advanced users working on additional extension projects @@ -7,177 +7,50 @@ that are built on top of Isaac Lab. Isaac Lab pip packages **does not** include training, inferencing, or running standalone workflows such as demos and examples. Therefore, users are required to define their own runner scripts when installing Isaac Lab from pip. -To learn about how to set up your own project on top of Isaac Lab, see :ref:`template-generator`. +To learn about how to set up your own project on top of Isaac Lab, please see :ref:`template-generator`. .. note:: - If you use Conda, we recommend using `Miniconda `_. - -- To use the pip installation approach for Isaac Lab, we recommend first creating a virtual environment. - Ensure that the python version of the virtual environment is **Python 3.11**. - - .. tab-set:: - - .. tab-item:: conda environment - - .. code-block:: bash - - conda create -n env_isaaclab python=3.11 - conda activate env_isaaclab - - .. tab-item:: uv environment - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - # create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab - # activate the virtual environment - source env_isaaclab/bin/activate - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code-block:: batch - - # create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab - # activate the virtual environment - env_isaaclab\Scripts\activate - - .. tab-item:: venv environment - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - # create a virtual environment named env_isaaclab with python3.11 - python3.11 -m venv env_isaaclab - # activate the virtual environment - source env_isaaclab/bin/activate - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code-block:: batch - - # create a virtual environment named env_isaaclab with python3.11 - python3.11 -m venv env_isaaclab - # activate the virtual environment - env_isaaclab\Scripts\activate - - -- Before installing Isaac Lab, ensure the latest pip version is installed. To update pip, run - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - pip install --upgrade pip - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows + Currently, we only provide pip packages for every major release of Isaac Lab. + For example, we provide the pip package for release 2.1.0 and 2.2.0, but not 2.1.1. + In the future, we will provide pip packages for every minor release of Isaac Lab. - .. code-block:: batch +.. include:: include/pip_python_virtual_env.rst - python -m pip install --upgrade pip +Installing dependencies +~~~~~~~~~~~~~~~~~~~~~~~ .. note:: - If you use uv, replace ``pip`` with ``uv pip``. - + In case you used UV to create your virtual environment, please replace ``pip`` with ``uv pip`` + in the following commands. -- Next, install a CUDA-enabled PyTorch 2.7.0 build for CUDA 12.8. +- Install a CUDA-enabled PyTorch 2.7.0 build for CUDA 12.8: - .. code-block:: bash + .. code-block:: none pip install torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 +- If you want to use ``rl_games`` for training and inferencing, install the + its Python 3.11 enabled fork: -- If using rl_games for training and inferencing, install the following python 3.11 enabled rl_games fork. - - .. code-block:: bash + .. code-block:: none pip install git+https://github.com/isaac-sim/rl_games.git@python3.11 -- Then, install the Isaac Lab packages, this will also install Isaac Sim. +- Install the Isaac Lab packages along with Isaac Sim: .. code-block:: none pip install isaaclab[isaacsim,all]==2.2.0 --extra-index-url https://pypi.nvidia.com -.. note:: - - Currently, we only provide pip packages for every major release of Isaac Lab. - For example, we provide the pip package for release 2.1.0 and 2.2.0, but not 2.1.1. - In the future, we will provide pip packages for every minor release of Isaac Lab. - - -Verifying the Isaac Sim installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Make sure that your virtual environment is activated (if applicable) - - -- Check that the simulator runs as expected: - - .. code:: bash - - # note: you can pass the argument "--help" to see all arguments possible. - isaacsim - -- It's also possible to run with a specific experience file, run: - - .. code:: bash - - # experience files can be absolute path, or relative path searched in isaacsim/apps or omni/apps - isaacsim isaacsim.exp.full.kit - - -.. attention:: - - When running Isaac Sim for the first time, all dependent extensions will be pulled from the registry. - This process can take upwards of 10 minutes and is required on the first run of each experience file. - Once the extensions are pulled, consecutive runs using the same experience file will use the cached extensions. - -.. attention:: - - The first run will prompt users to accept the Nvidia Omniverse License Agreement. - To accept the EULA, reply ``Yes`` when prompted with the below message: - - .. code:: bash - - By installing or using Isaac Sim, I agree to the terms of NVIDIA OMNIVERSE LICENSE AGREEMENT (EULA) - in https://docs.isaacsim.omniverse.nvidia.com/latest/common/NVIDIA_Omniverse_License_Agreement.html - - Do you accept the EULA? (Yes/No): Yes - - -If the simulator does not run or crashes while following the above -instructions, it means that something is incorrectly configured. To -debug and troubleshoot, please check Isaac Sim -`documentation `__ -and the -`forums `__. +.. include:: include/pip_verify_isaacsim.rst Running Isaac Lab Scripts ~~~~~~~~~~~~~~~~~~~~~~~~~ -By following the above scripts, your python environment should now have access to all of the Isaac Lab extensions. +By following the above scripts, your Python environment should now have access to all of the Isaac Lab extensions. To run a user-defined script for Isaac Lab, simply run .. code:: bash diff --git a/docs/source/setup/installation/pip_installation.rst b/docs/source/setup/installation/pip_installation.rst index 1337df6d9cb..3e718f78349 100644 --- a/docs/source/setup/installation/pip_installation.rst +++ b/docs/source/setup/installation/pip_installation.rst @@ -1,420 +1,66 @@ .. _isaaclab-pip-installation: -Installation using Isaac Sim Pip -================================ +Installation using Isaac Sim Pip Package +======================================== -Isaac Lab requires Isaac Sim. This tutorial first installs Isaac Sim from pip, then Isaac Lab from source code. - -Installing Isaac Sim --------------------- - -From Isaac Sim 4.0 release, it is possible to install Isaac Sim using pip. -This approach makes it easier to install Isaac Sim without requiring to download the Isaac Sim binaries. -If you encounter any issues, please report them to the -`Isaac Sim Forums `_. +The following steps first installs Isaac Sim from pip, then Isaac Lab from source code. .. attention:: Installing Isaac Sim with pip requires GLIBC 2.35+ version compatibility. To check the GLIBC version on your system, use command ``ldd --version``. - This may pose compatibility issues with some Linux distributions. For instance, Ubuntu 20.04 LTS has GLIBC 2.31 - by default. If you encounter compatibility issues, we recommend following the + This may pose compatibility issues with some Linux distributions. For instance, Ubuntu 20.04 LTS + has GLIBC 2.31 by default. If you encounter compatibility issues, we recommend following the :ref:`Isaac Sim Binaries Installation ` approach. -.. attention:: - - For details on driver requirements, please see the `Technical Requirements `_ guide! - - On Windows, it may be necessary to `enable long path support `_ to avoid installation errors due to OS limitations. - -.. attention:: +.. note:: If you plan to :ref:`Set up Visual Studio Code ` later, we recommend following the :ref:`Isaac Sim Binaries Installation ` approach. -.. note:: - - If you use Conda, we recommend using `Miniconda `_. - -- To use the pip installation approach for Isaac Sim, we recommend first creating a virtual environment. - Ensure that the python version of the virtual environment is **Python 3.11**. - - .. tab-set:: - - .. tab-item:: conda environment - - .. code-block:: bash - - conda create -n env_isaaclab python=3.11 - conda activate env_isaaclab - - .. tab-item:: uv environment - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - # create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab - # activate the virtual environment - source env_isaaclab/bin/activate - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code-block:: batch - - # create a virtual environment named env_isaaclab with python3.11 - uv venv --python 3.11 env_isaaclab - # activate the virtual environment - env_isaaclab\Scripts\activate - - .. tab-item:: venv environment - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code-block:: bash - - # create a virtual environment named env_isaaclab with python3.11 - python3.11 -m venv env_isaaclab - # activate the virtual environment - source env_isaaclab/bin/activate - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code-block:: batch - - # create a virtual environment named env_isaaclab with python3.11 - python3.11 -m venv env_isaaclab - # activate the virtual environment - env_isaaclab\Scripts\activate - - -- Before installing Isaac Sim, ensure the latest pip version is installed. To update pip, run - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux +Installing Isaac Sim +-------------------- - .. code-block:: bash +From Isaac Sim 4.0 onwards, it is possible to install Isaac Sim using pip. +This approach makes it easier to install Isaac Sim without requiring to download the Isaac Sim binaries. +If you encounter any issues, please report them to the +`Isaac Sim Forums `_. - pip install --upgrade pip +.. attention:: - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows + On Windows, it may be necessary to `enable long path support `_ + to avoid installation errors due to OS limitations. - .. code-block:: batch +.. include:: include/pip_python_virtual_env.rst - python -m pip install --upgrade pip +Installing dependencies +~~~~~~~~~~~~~~~~~~~~~~~ .. note:: - If you use uv, replace ``pip`` with ``uv pip``. + In case you used UV to create your virtual environment, please replace ``pip`` with ``uv pip`` + in the following commands. -- Next, install a CUDA-enabled PyTorch 2.7.0 build. +- Install a CUDA-enabled PyTorch 2.7.0 build for CUDA 12.8: .. code-block:: bash pip install torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 - -- Then, install the Isaac Sim packages. +- Install Isaac Sim pip packages: .. code-block:: none pip install "isaacsim[all,extscache]==5.0.0" --extra-index-url https://pypi.nvidia.com - -Verifying the Isaac Sim installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- Make sure that your virtual environment is activated (if applicable) - - -- Check that the simulator runs as expected: - - .. code:: bash - - # note: you can pass the argument "--help" to see all arguments possible. - isaacsim - -- It's also possible to run with a specific experience file, run: - - .. code:: bash - - # experience files can be absolute path, or relative path searched in isaacsim/apps or omni/apps - isaacsim isaacsim.exp.full.kit - - -.. attention:: - - When running Isaac Sim for the first time, all dependent extensions will be pulled from the registry. - This process can take upwards of 10 minutes and is required on the first run of each experience file. - Once the extensions are pulled, consecutive runs using the same experience file will use the cached extensions. - -.. attention:: - - The first run will prompt users to accept the NVIDIA Software License Agreement. - To accept the EULA, reply ``Yes`` when prompted with the below message: - - .. code:: bash - - By installing or using Isaac Sim, I agree to the terms of NVIDIA SOFTWARE LICENSE AGREEMENT (EULA) - in https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-software-license-agreement - - Do you accept the EULA? (Yes/No): Yes - - -If the simulator does not run or crashes while following the above -instructions, it means that something is incorrectly configured. To -debug and troubleshoot, please check Isaac Sim -`documentation `__ -and the -`forums `__. - - +.. include:: include/pip_verify_isaacsim.rst Installing Isaac Lab -------------------- -Cloning Isaac Lab -~~~~~~~~~~~~~~~~~ - -.. note:: - - We recommend making a `fork `_ of the Isaac Lab repository to contribute - to the project but this is not mandatory to use the framework. If you - make a fork, please replace ``isaac-sim`` with your username - in the following instructions. - -Clone the Isaac Lab repository into your workspace: - -.. tab-set:: - - .. tab-item:: SSH - - .. code:: bash - - git clone git@github.com:isaac-sim/IsaacLab.git - - .. tab-item:: HTTPS - - .. code:: bash - - git clone https://github.com/isaac-sim/IsaacLab.git - - -.. note:: - We provide a helper executable `isaaclab.sh `_ that provides - utilities to manage extensions: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: text - - ./isaaclab.sh --help - - usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -o, --docker Run the docker container helper script (docker/container.sh). - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. - -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: text - - isaaclab.bat --help - - usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. - -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. - -Installation -~~~~~~~~~~~~ - -- Install dependencies using ``apt`` (on Ubuntu): - - .. code:: bash - - sudo apt install cmake build-essential - -- Run the install command that iterates over all the extensions in ``source`` directory and installs them - using pip (with ``--editable`` flag): - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh --install # or "./isaaclab.sh -i" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: bash - - isaaclab.bat --install :: or "isaaclab.bat -i" - -.. note:: - - By default, this will install all the learning frameworks. If you want to install only a specific framework, you can - pass the name of the framework as an argument. For example, to install only the ``rl_games`` framework, you can run - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: bash - - isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" - - The valid options are ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, ``none``. - - -Verifying the Isaac Lab installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To verify that the installation was successful, run the following command from the -top of the repository: - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Using the isaaclab.sh executable - # note: this works for both the bundled python and the virtual environment - ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py - - # Option 2: Using python in your virtual environment - python scripts/tutorials/00_sim/create_empty.py - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Using the isaaclab.bat executable - :: note: this works for both the bundled python and the virtual environment - isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py - - :: Option 2: Using python in your virtual environment - python scripts\tutorials\00_sim\create_empty.py - - -The above command should launch the simulator and display a window with a black -viewport as shown below. You can exit the script by pressing ``Ctrl+C`` on your terminal. -On Windows machines, please terminate the process from Command Prompt using -``Ctrl+Break`` or ``Ctrl+fn+B``. - - -.. figure:: ../../_static/setup/verify_install.jpg - :align: center - :figwidth: 100% - :alt: Simulator with a black window. - - -If you see this, then the installation was successful! |:tada:| - -Train a robot! -~~~~~~~~~~~~~~ - -You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! -We recommend adding ``--headless`` for faster training. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless - -... Or a robot dog! - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless +.. include:: include/src_clone_isaaclab.rst -Isaac Lab provides the tools you'll need to create your own **Tasks** and **Workflows** for whatever your project needs may be. Take a look at our :ref:`how-to` guides like `Adding your own learning Library `_ or `Wrapping Environments `_ for details. +.. include:: include/src_build_isaaclab.rst -.. figure:: ../../_static/setup/isaac_ants_example.jpg - :align: center - :figwidth: 100% - :alt: Idle hands... +.. include:: include/src_verify_isaaclab.rst diff --git a/docs/source/setup/installation/source_installation.rst b/docs/source/setup/installation/source_installation.rst index 7f7cd232154..c697c1dd205 100644 --- a/docs/source/setup/installation/source_installation.rst +++ b/docs/source/setup/installation/source_installation.rst @@ -1,9 +1,9 @@ .. _isaaclab-source-installation: -Installation using Isaac Sim Source -=================================== +Installation using Isaac Sim Source Code +======================================== -Isaac Lab requires Isaac Sim. This tutorial first installs Isaac Sim from source, then Isaac Lab from source code. +The following steps first installs Isaac Sim from source, then Isaac Lab from source code. .. note:: @@ -13,6 +13,9 @@ Isaac Lab requires Isaac Sim. This tutorial first installs Isaac Sim from source Installing Isaac Sim -------------------- +Building from source +~~~~~~~~~~~~~~~~~~~~ + From Isaac Sim 5.0 release, it is possible to build Isaac Sim from its source code. This approach is meant for users who wish to modify the source code of Isaac Sim as well, or want to test Isaac Lab with the nightly version of Isaac Sim. @@ -62,7 +65,6 @@ for the convenience of users. Verifying the Isaac Sim installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - To avoid the overhead of finding and locating the Isaac Sim installation directory every time, we recommend exporting the following environment variables to your terminal for the remaining of the installation instructions: @@ -90,452 +92,18 @@ variables to your terminal for the remaining of the installation instructions: :: Isaac Sim python executable set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" -- Check that the simulator runs as expected: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # note: you can pass the argument "--help" to see all arguments possible. - ${ISAACSIM_PATH}/isaac-sim.sh - - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: note: you can pass the argument "--help" to see all arguments possible. - %ISAACSIM_PATH%\isaac-sim.bat - - -- Check that the simulator runs from a standalone python script: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # checks that python path is set correctly - ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')" - # checks that Isaac Sim can be launched from python - ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/isaacsim.core.api/add_cubes.py - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: checks that python path is set correctly - %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')" - :: checks that Isaac Sim can be launched from python - %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\isaacsim.core.api\add_cubes.py - -.. caution:: - - If you have been using a previous version of Isaac Sim, you need to run the following command for the *first* - time after installation to remove all the old user data and cached variables: - - .. tab-set:: - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - - .. code:: bash - - ${ISAACSIM_PATH}/isaac-sim.sh --reset-user - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - - .. code:: batch - - %ISAACSIM_PATH%\isaac-sim.bat --reset-user - - -If the simulator does not run or crashes while following the above -instructions, it means that something is incorrectly configured. To -debug and troubleshoot, please check Isaac Sim -`documentation `__ -and the -`forums `__. +.. include:: include/bin_verify_isaacsim.rst Installing Isaac Lab -------------------- -Cloning Isaac Lab -~~~~~~~~~~~~~~~~~ - -.. note:: - - We recommend making a `fork `_ of the Isaac Lab repository to contribute - to the project but this is not mandatory to use the framework. If you - make a fork, please replace ``isaac-sim`` with your username - in the following instructions. - -Clone the Isaac Lab repository into your **workspace**. Please note that the location of the Isaac Lab repository -should be outside of the Isaac Sim repository. For example, if you cloned Isaac Sim into ``~/IsaacSim``, -then you should clone Isaac Lab into ``~/IsaacLab``. - -.. tab-set:: - - .. tab-item:: SSH - - .. code:: bash - - git clone git@github.com:isaac-sim/IsaacLab.git - - .. tab-item:: HTTPS - - .. code:: bash - - git clone https://github.com/isaac-sim/IsaacLab.git - - -.. note:: - We provide a helper executable `isaaclab.sh `_ that provides - utilities to manage extensions: - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: text - - ./isaaclab.sh --help - - usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -o, --docker Run the docker container helper script (docker/container.sh). - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. - -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: text - - isaaclab.bat --help - - usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-n] [-c] -- Utility to manage Isaac Lab. - - optional arguments: - -h, --help Display the help content. - -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. - -f, --format Run pre-commit to format the code and check lints. - -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). - -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. - -t, --test Run all python pytest tests. - -v, --vscode Generate the VSCode settings file from template. - -d, --docs Build the documentation from source using sphinx. - -n, --new Create a new external project or internal task from template. - -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'env_isaaclab'. - -u, --uv [NAME] Create the uv environment for Isaac Lab. Default name is 'env_isaaclab'. - - -Creating the Isaac Sim Symbolic Link -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Set up a symbolic link between the installed Isaac Sim root folder -and ``_isaac_sim`` in the Isaac Lab directory. This makes it convenient -to index the python modules and look for extensions shipped with Isaac Sim. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # enter the cloned repository - cd IsaacLab - # create a symbolic link - ln -s ${ISAACSIM_PATH} _isaac_sim - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: enter the cloned repository - cd IsaacLab - :: create a symbolic link - requires launching Command Prompt with Administrator access - mklink /D _isaac_sim ${ISAACSIM_PATH} - - -Setting up the uv environment (optional) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. attention:: - This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. - -The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac -Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable -behaves like a python executable, and can be used to run any python script or -module with the simulator. For more information, please refer to the -`documentation `__. - -To install ``uv``, please follow the instructions `here `__. -You can create the Isaac Lab environment using the following commands. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Default name for uv environment is 'env_isaaclab' - ./isaaclab.sh --uv # or "./isaaclab.sh -u" - # Option 2: Custom name for uv environment - ./isaaclab.sh --uv my_env # or "./isaaclab.sh -u my_env" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Default name for uv environment is 'env_isaaclab' - isaaclab.bat --uv :: or "isaaclab.bat -u" - :: Option 2: Custom name for uv environment - isaaclab.bat --uv my_env :: or "isaaclab.bat -u my_env" - - -Once created, be sure to activate the environment before proceeding! - -.. code:: bash - - source ./env_isaaclab/bin/activate # or "source ./my_env/bin/activate" - -Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` -to run python scripts. You can use the default python executable in your environment -by running ``python`` or ``python3``. However, for the rest of the documentation, -we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command -is equivalent to running ``python`` or ``python3`` in your virtual environment. - - -Setting up the conda environment (optional) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. attention:: - This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. - -.. note:: - - If you use Conda, we recommend using `Miniconda `_. - -The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac -Sim, using ``./isaaclab.sh -p`` command (unless inside a virtual environment). This executable -behaves like a python executable, and can be used to run any python script or -module with the simulator. For more information, please refer to the -`documentation `__. +.. include:: include/src_clone_isaaclab.rst -To install ``conda``, please follow the instructions `here `__. -You can create the Isaac Lab environment using the following commands. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Default name for conda environment is 'env_isaaclab' - ./isaaclab.sh --conda # or "./isaaclab.sh -c" - # Option 2: Custom name for conda environment - ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Default name for conda environment is 'env_isaaclab' - isaaclab.bat --conda :: or "isaaclab.bat -c" - :: Option 2: Custom name for conda environment - isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" - - -Once created, be sure to activate the environment before proceeding! - -.. code:: bash - - conda activate env_isaaclab # or "conda activate my_env" - -Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` -to run python scripts. You can use the default python executable in your environment -by running ``python`` or ``python3``. However, for the rest of the documentation, -we will assume that you are using ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command -is equivalent to running ``python`` or ``python3`` in your virtual environment. - -Installation -~~~~~~~~~~~~ - -- Install dependencies using ``apt`` (on Ubuntu): - - .. code:: bash - - sudo apt install cmake build-essential - -- Run the install command that iterates over all the extensions in ``source`` directory and installs them - using pip (with ``--editable`` flag): - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh --install # or "./isaaclab.sh -i" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: bash - - isaaclab.bat --install :: or "isaaclab.bat -i" - -.. note:: - - By default, this will install all the learning frameworks. If you want to install only a specific framework, you can - pass the name of the framework as an argument. For example, to install only the ``rl_games`` framework, you can run - - .. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: bash - - isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" - - The valid options are ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, ``none``. - - -Verifying the Isaac Lab installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To verify that the installation was successful, run the following command from the -top of the repository: - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - # Option 1: Using the isaaclab.sh executable - # note: this works for both the bundled python and the virtual environment - ./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py - - # Option 2: Using python in your virtual environment - python scripts/tutorials/00_sim/create_empty.py - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - :: Option 1: Using the isaaclab.bat executable - :: note: this works for both the bundled python and the virtual environment - isaaclab.bat -p scripts\tutorials\00_sim\create_empty.py - - :: Option 2: Using python in your virtual environment - python scripts\tutorials\00_sim\create_empty.py - - -The above command should launch the simulator and display a window with a black -viewport as shown below. You can exit the script by pressing ``Ctrl+C`` on your terminal. -On Windows machines, please terminate the process from Command Prompt using -``Ctrl+Break`` or ``Ctrl+fn+B``. - - -.. figure:: ../../_static/setup/verify_install.jpg - :align: center - :figwidth: 100% - :alt: Simulator with a black window. - - -If you see this, then the installation was successful! |:tada:| - -Train a robot! -~~~~~~~~~~~~~~ - -You can now use Isaac Lab to train a robot through Reinforcement Learning! The quickest way to use Isaac Lab is through the predefined workflows using one of our **Batteries-included** robot tasks. Execute the following command to quickly train an ant to walk! -We recommend adding ``--headless`` for faster training. - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch - - isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Ant-v0 --headless - -... Or a robot dog! - -.. tab-set:: - :sync-group: os - - .. tab-item:: :icon:`fa-brands fa-linux` Linux - :sync: linux - - .. code:: bash - - ./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless - - .. tab-item:: :icon:`fa-brands fa-windows` Windows - :sync: windows - - .. code:: batch +.. include:: include/src_symlink_isaacsim.rst - isaaclab.bat -p scripts/reinforcement_learning/rsl_rl/train.py --task=Isaac-Velocity-Rough-Anymal-C-v0 --headless +.. include:: include/src_python_virtual_env.rst -Isaac Lab provides the tools you'll need to create your own **Tasks** and **Workflows** for whatever your project needs may be. Take a look at our :ref:`how-to` guides like `Adding your own learning Library `_ or `Wrapping Environments `_ for details. +.. include:: include/src_build_isaaclab.rst -.. figure:: ../../_static/setup/isaac_ants_example.jpg - :align: center - :figwidth: 100% - :alt: Idle hands... +.. include:: include/src_verify_isaaclab.rst From 7455d3dfe743344bbf7f0ac6385caf5884489539 Mon Sep 17 00:00:00 2001 From: Lorenz Wellhausen Date: Thu, 18 Sep 2025 20:56:24 +0200 Subject: [PATCH 45/50] Fix PDActuator docstring to match actual implementation (#3493) # Description The docstring of the `IdealPDActuator` didn't match its implementation. Desired and actual joint positions and velocities were swapped. Actual implementation is like this: ``` def compute( self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor ) -> ArticulationActions: # compute errors error_pos = control_action.joint_positions - joint_pos error_vel = control_action.joint_velocities - joint_vel # calculate the desired joint torques self.computed_effort = self.stiffness * error_pos + self.damping * error_vel + control_action.joint_efforts ``` It is "`desired - current`", the current docstring says the opposite: image ## Type of change - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there Co-authored-by: Lorenz Wellhausen --- source/isaaclab/isaaclab/actuators/actuator_pd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd.py b/source/isaaclab/isaaclab/actuators/actuator_pd.py index 11c39f20177..162005dfd17 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_pd.py +++ b/source/isaaclab/isaaclab/actuators/actuator_pd.py @@ -152,7 +152,7 @@ class IdealPDActuator(ActuatorBase): .. math:: - \tau_{j, computed} = k_p * (q - q_{des}) + k_d * (\dot{q} - \dot{q}_{des}) + \tau_{ff} + \tau_{j, computed} = k_p * (q_{des} - q) + k_d * (\dot{q}_{des} - \dot{q}) + \tau_{ff} where, :math:`k_p` and :math:`k_d` are joint stiffness and damping gains, :math:`q` and :math:`\dot{q}` are the current joint positions and velocities, :math:`q_{des}`, :math:`\dot{q}_{des}` and :math:`\tau_{ff}` From 187f9a5889c10971fbdb3b700c5248f9df90bdaa Mon Sep 17 00:00:00 2001 From: Michael Gussert Date: Fri, 19 Sep 2025 15:01:56 -0700 Subject: [PATCH 46/50] Fixes broken links in the documentation (#3500) ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- docs/source/how-to/add_own_library.rst | 2 ++ docs/source/how-to/import_new_asset.rst | 4 ++-- docs/source/how-to/master_omniverse.rst | 1 - docs/source/overview/developer-guide/vs_code.rst | 3 +-- docs/source/refs/contributing.rst | 7 ++----- docs/source/refs/reference_architecture/index.rst | 15 ++++++++------- docs/source/refs/release_notes.rst | 4 ++-- docs/source/tutorials/00_sim/launch_app.rst | 2 +- docs/source/tutorials/01_assets/add_new_robot.rst | 2 +- source/isaaclab/isaaclab/app/app_launcher.py | 2 +- source/isaaclab/isaaclab/envs/__init__.py | 2 +- .../isaaclab/isaaclab/scene/interactive_scene.py | 2 +- .../sensors/contact_sensor/contact_sensor.py | 2 +- .../sensors/ray_caster/patterns/patterns.py | 2 +- .../isaaclab/sim/converters/mesh_converter.py | 2 +- .../isaaclab/isaaclab/sim/simulation_context.py | 2 +- .../isaaclab/sim/spawners/materials/__init__.py | 2 +- .../spawners/materials/physics_materials_cfg.py | 7 ------- .../isaaclab/sim/spawners/sensors/sensors_cfg.py | 4 ---- .../isaaclab/sim/spawners/shapes/shapes.py | 2 +- source/isaaclab/isaaclab/sim/utils.py | 2 +- 21 files changed, 29 insertions(+), 42 deletions(-) diff --git a/docs/source/how-to/add_own_library.rst b/docs/source/how-to/add_own_library.rst index d0cb9dd5a62..e84578fca52 100644 --- a/docs/source/how-to/add_own_library.rst +++ b/docs/source/how-to/add_own_library.rst @@ -1,3 +1,5 @@ +.. _how-to-add-library: + Adding your own learning library ================================ diff --git a/docs/source/how-to/import_new_asset.rst b/docs/source/how-to/import_new_asset.rst index 9d2f828ad40..41eacc48673 100644 --- a/docs/source/how-to/import_new_asset.rst +++ b/docs/source/how-to/import_new_asset.rst @@ -307,8 +307,8 @@ of gravity. .. _instanceable: https://openusd.org/dev/api/_usd__page__scenegraph_instancing.html .. _documentation: https://docs.isaacsim.omniverse.nvidia.com/latest/isaac_lab_tutorials/tutorial_instanceable_assets.html -.. _MJCF importer: https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/import_mjcf.html -.. _URDF importer: https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/import_urdf.html +.. _MJCF importer: https://docs.isaacsim.omniverse.nvidia.com/latest/importer_exporter/ext_isaacsim_asset_importer_mjcf.html +.. _URDF importer: https://docs.isaacsim.omniverse.nvidia.com/latest/importer_exporter/ext_isaacsim_asset_importer_urdf.html .. _anymal.urdf: https://github.com/isaac-orbit/anymal_d_simple_description/blob/master/urdf/anymal.urdf .. _asset converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_asset-converter.html .. _mujoco_menagerie: https://github.com/google-deepmind/mujoco_menagerie/tree/main/unitree_h1 diff --git a/docs/source/how-to/master_omniverse.rst b/docs/source/how-to/master_omniverse.rst index 0108ab64821..7360e8798cb 100644 --- a/docs/source/how-to/master_omniverse.rst +++ b/docs/source/how-to/master_omniverse.rst @@ -119,6 +119,5 @@ Part 3: More Resources - `Omniverse Glossary of Terms `__ - `Omniverse Code Samples `__ -- `PhysX Collider Compatibility `__ - `PhysX Limitations `__ - `PhysX Documentation `__. diff --git a/docs/source/overview/developer-guide/vs_code.rst b/docs/source/overview/developer-guide/vs_code.rst index 6e69ab407d4..1b7190c341b 100644 --- a/docs/source/overview/developer-guide/vs_code.rst +++ b/docs/source/overview/developer-guide/vs_code.rst @@ -52,8 +52,7 @@ If everything executes correctly, it should create the following files: For more information on VSCode support for Omniverse, please refer to the following links: -* `Isaac Sim VSCode support `__ -* `Debugging with VSCode `__ +* `Isaac Sim VSCode support `__ Configuring the python interpreter diff --git a/docs/source/refs/contributing.rst b/docs/source/refs/contributing.rst index 40c84619655..bc2d60c426a 100644 --- a/docs/source/refs/contributing.rst +++ b/docs/source/refs/contributing.rst @@ -115,11 +115,8 @@ integrated with the `NVIDIA Omniverse Platform `__. -To use this content, you can use the Asset Browser provided in Isaac Sim. - -Please check the `Isaac Sim documentation `__ -for more information on how to download the assets. +Please checkout the `Isaac Sim Assets `__ +for more information on what is presently available. .. attention:: diff --git a/docs/source/refs/reference_architecture/index.rst b/docs/source/refs/reference_architecture/index.rst index 34296144506..6aad8b4156e 100644 --- a/docs/source/refs/reference_architecture/index.rst +++ b/docs/source/refs/reference_architecture/index.rst @@ -195,10 +195,12 @@ Some wrappers include: * `Video Wrappers `__ * `RL Libraries Wrappers `__ +.. currentmodule:: isaaclab_rl + Most RL libraries expect their own variation of an environment interface. This means the data types needed by each library differs. Isaac Lab provides its own wrappers to convert the environment into the expected interface by the RL library a user wants to use. These are -specified in the `Isaac Lab utils wrapper module `__. +specified in :class:`isaaclab_rl` See the `full list `__ of other wrappers APIs. For more information on how these wrappers work, please refer to the `Wrapping environments `__ documentation. @@ -345,7 +347,7 @@ Check out our resources on using Isaac Lab with your robots. Review Our Documentation & Samples Resources -* `Isaac Lab Tutorials`_ +* :ref:`Isaac Lab Tutorials ` * `Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab`_ * `Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab`_ * `Closing the Sim-to-Real Gap: Training Spot Quadruped Locomotion with NVIDIA Isaac Lab `__ @@ -360,16 +362,15 @@ Learn More About Featured NVIDIA Solutions .. _curriculum learning: https://arxiv.org/abs/2109.11978 .. _CAD Converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_cad-converter.html -.. _URDF Importer: https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/ext_isaacsim_asset_importer_urdf.html -.. _MJCF Importer: https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/ext_isaacsim_asset_importer_mjcf.html +.. _URDF Importer: https://docs.isaacsim.omniverse.nvidia.com/latest/importer_exporter/ext_isaacsim_asset_importer_urdf.html +.. _MJCF Importer: https://docs.isaacsim.omniverse.nvidia.com/latest/importer_exporter/ext_isaacsim_asset_importer_mjcf.html .. _Onshape Importer: https://docs.omniverse.nvidia.com/extensions/latest/ext_onshape.html -.. _Isaac Sim Reference Architecture: https://docs.isaacsim.omniverse.nvidia.com/latest/isaac_sim_reference_architecture.html -.. _Importing Assets section: https://docs.isaacsim.omniverse.nvidia.com/latest/isaac_sim_reference_architecture.html#importing-assets +.. _Isaac Sim Reference Architecture: https://docs.isaacsim.omniverse.nvidia.com/latest/introduction/reference_architecture.html +.. _Importing Assets section: https://docs.isaacsim.omniverse.nvidia.com/latest/importer_exporter/importers_exporters.html .. _Scale AI-Enabled Robotics Development Workloads with NVIDIA OSMO: https://developer.nvidia.com/blog/scale-ai-enabled-robotics-development-workloads-with-nvidia-osmo/ .. _Isaac Perceptor: https://developer.nvidia.com/isaac/perceptor .. _Isaac Manipulator: https://developer.nvidia.com/isaac/manipulator .. _Additional Resources: https://isaac-sim.github.io/IsaacLab/main/source/refs/additional_resources.html -.. _Isaac Lab Tutorials: file:///home/oomotuyi/isaac/IsaacLab/docs/_build/current/source/tutorials/index.html .. _Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab: https://developer.nvidia.com/blog/fast-track-robot-learning-in-simulation-using-nvidia-isaac-lab/ .. _Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab: https://developer.nvidia.com/blog/supercharge-robotics-workflows-with-ai-and-simulation-using-nvidia-isaac-sim-4-0-and-nvidia-isaac-lab/ diff --git a/docs/source/refs/release_notes.rst b/docs/source/refs/release_notes.rst index be9dc4d8ec1..94350097756 100644 --- a/docs/source/refs/release_notes.rst +++ b/docs/source/refs/release_notes.rst @@ -1361,12 +1361,12 @@ New Features * Integrated CI/CD pipeline, which is triggered on pull requests and publishes the results publicly * Extended support for Windows OS platforms -* Added `tiled rendered `_ based Camera +* Added tiled render based Camera sensor implementation. This provides optimized RGB-D rendering throughputs of up to 10k frames per second. * Added support for multi-GPU and multi-node training for the RL-Games library * Integrated APIs for environment designing (direct workflow) without relying on managers * Added implementation of delayed PD actuator model -* `Added various new learning environments `_: +* Added various new learning environments: * Cartpole balancing using images * Shadow hand cube reorientation * Boston Dynamics Spot locomotion diff --git a/docs/source/tutorials/00_sim/launch_app.rst b/docs/source/tutorials/00_sim/launch_app.rst index 8013e9975c3..05fa32c4648 100644 --- a/docs/source/tutorials/00_sim/launch_app.rst +++ b/docs/source/tutorials/00_sim/launch_app.rst @@ -172,5 +172,5 @@ want our simulation to be more performant. The process can be killed by pressing terminal. -.. _specification: https://docs.omniverse.nvidia.com/py/isaacsim/source/isaacsim.simulation_app/docs/index.html#isaacsim.simulation_app.SimulationApp.DEFAULT_LAUNCHER_CONFIG +.. _specification: https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.simulation_app/docs/index.html#isaacsim.simulation_app.SimulationApp.DEFAULT_LAUNCHER_CONFIG .. _WebRTC Livestreaming: https://docs.isaacsim.omniverse.nvidia.com/latest/installation/manual_livestream_clients.html#isaac-sim-short-webrtc-streaming-client diff --git a/docs/source/tutorials/01_assets/add_new_robot.rst b/docs/source/tutorials/01_assets/add_new_robot.rst index 61664cef518..a4d258f82c1 100644 --- a/docs/source/tutorials/01_assets/add_new_robot.rst +++ b/docs/source/tutorials/01_assets/add_new_robot.rst @@ -6,7 +6,7 @@ Adding a New Robot to Isaac Lab .. currentmodule:: isaaclab Simulating and training a new robot is a multi-step process that starts with importing the robot into Isaac Sim. -This is covered in depth in the Isaac Sim documentation `here `_. +This is covered in depth in the Isaac Sim documentation `here `_. Once the robot is imported and tuned for simulation, we must define those interfaces necessary to clone the robot across multiple environments, drive its joints, and properly reset it, regardless of the chosen workflow or training framework. diff --git a/source/isaaclab/isaaclab/app/app_launcher.py b/source/isaaclab/isaaclab/app/app_launcher.py index aa97e2bc3ec..1ecfbdd8642 100644 --- a/source/isaaclab/isaaclab/app/app_launcher.py +++ b/source/isaaclab/isaaclab/app/app_launcher.py @@ -76,7 +76,7 @@ def __init__(self, launcher_args: argparse.Namespace | dict | None = None, **kwa such as ``LIVESTREAM``. .. _argparse.Namespace: https://docs.python.org/3/library/argparse.html?highlight=namespace#argparse.Namespace - .. _SimulationApp: https://docs.omniverse.nvidia.com/py/isaacsim/source/isaacsim.simulation_app/docs/index.html + .. _SimulationApp: https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.simulation_app/docs/index.html#isaacsim.simulation_app.SimulationApp """ # We allow users to pass either a dict or an argparse.Namespace into # __init__, anticipating that these will be all of the argparse arguments diff --git a/source/isaaclab/isaaclab/envs/__init__.py b/source/isaaclab/isaaclab/envs/__init__.py index e69aba9d25c..2d274b8adad 100644 --- a/source/isaaclab/isaaclab/envs/__init__.py +++ b/source/isaaclab/isaaclab/envs/__init__.py @@ -39,7 +39,7 @@ For more information about the workflow design patterns, see the `Task Design Workflows`_ section. -.. _`Task Design Workflows`: https://isaac-sim.github.io/IsaacLab/source/features/task_workflows.html +.. _`Task Design Workflows`: https://docs.isaacsim.omniverse.nvidia.com/latest/introduction/workflows.html """ from . import mdp, ui diff --git a/source/isaaclab/isaaclab/scene/interactive_scene.py b/source/isaaclab/isaaclab/scene/interactive_scene.py index e12118a36d0..15739c33ad7 100644 --- a/source/isaaclab/isaaclab/scene/interactive_scene.py +++ b/source/isaaclab/isaaclab/scene/interactive_scene.py @@ -421,7 +421,7 @@ def extras(self) -> dict[str, XFormPrim]: These are not reset or updated by the scene. They are mainly other prims that are not necessarily handled by the interactive scene, but are useful to be accessed by the user. - .. _XFormPrim: https://docs.omniverse.nvidia.com/py/isaacsim/source/isaacsim.core/docs/index.html#isaacsim.core.prims.XFormPrim + .. _XFormPrim: https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.core.prims/docs/index.html#isaacsim.core.prims.XFormPrim """ return self._extras diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py index ba2a019ef64..aed50d390f8 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/contact_sensor.py @@ -58,7 +58,7 @@ class ContactSensor(SensorBase): it against the object. .. _PhysX ContactReporter: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_contact_report_a_p_i.html - .. _RigidContact: https://docs.omniverse.nvidia.com/py/isaacsim/source/isaacsim.core/docs/index.html#isaacsim.core.prims.RigidContact + .. _RigidContact: https://docs.isaacsim.omniverse.nvidia.com/latest/py/source/extensions/isaacsim.core.api/docs/index.html#isaacsim.core.api.sensors.RigidContactView """ cfg: ContactSensorCfg diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py index 1a09f8b626f..2a6a438d178 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/patterns/patterns.py @@ -109,7 +109,7 @@ def bpearl_pattern(cfg: patterns_cfg.BpearlPatternCfg, device: str) -> tuple[tor The `Robosense RS-Bpearl`_ is a short-range LiDAR that has a 360 degrees x 90 degrees super wide field of view. It is designed for near-field blind-spots detection. - .. _Robosense RS-Bpearl: https://www.roscomponents.com/en/lidar-laser-scanner/267-rs-bpearl.html + .. _Robosense RS-Bpearl: https://www.roscomponents.com/product/rs-bpearl/ Args: cfg: The configuration instance for the pattern. diff --git a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py index c6c4683bbb8..a35167fad99 100644 --- a/source/isaaclab/isaaclab/sim/converters/mesh_converter.py +++ b/source/isaaclab/isaaclab/sim/converters/mesh_converter.py @@ -29,7 +29,7 @@ class MeshConverter(AssetConverterBase): instancing and physics work. The rigid body component must be added to each instance and not the referenced asset (i.e. the prototype prim itself). This is because the rigid body component defines properties that are specific to each instance and cannot be shared under the referenced asset. For - more information, please check the `documentation `_. + more information, please check the `documentation `_. Due to the above, we follow the following structure: diff --git a/source/isaaclab/isaaclab/sim/simulation_context.py b/source/isaaclab/isaaclab/sim/simulation_context.py index fd23d73c01c..fbd7f0043e9 100644 --- a/source/isaaclab/isaaclab/sim/simulation_context.py +++ b/source/isaaclab/isaaclab/sim/simulation_context.py @@ -783,7 +783,7 @@ def _set_additional_physx_params(self): # create the default physics material # this material is used when no material is specified for a primitive - # check: https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/simulation-control/physics-settings.html#physics-materials + # check: https://isaac-sim.github.io/IsaacLab/main/source/api/lab/isaaclab.sim.html#isaaclab.sim.SimulationCfg.physics_material material_path = f"{self.cfg.physics_prim_path}/defaultMaterial" self.cfg.physics_material.func(material_path, self.cfg.physics_material) # bind the physics material to the scene diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py b/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py index 052a2be4f88..966efec76b8 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/__init__.py @@ -49,7 +49,7 @@ .. _Material Definition Language (MDL): https://raytracing-docs.nvidia.com/mdl/introduction/index.html#mdl_introduction# .. _Materials: https://docs.omniverse.nvidia.com/materials-and-rendering/latest/materials.html -.. _physics material: https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/simulation-control/physics-settings.html#physics-materials +.. _physics material: https://isaac-sim.github.io/IsaacLab/main/source/api/lab/isaaclab.sim.html#isaaclab.sim.SimulationCfg.physics_material .. _USD Material Binding API: https://openusd.org/dev/api/class_usd_shade_material_binding_a_p_i.html .. _Physics Scene: https://openusd.org/dev/api/usd_physics_page_front.html """ diff --git a/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py b/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py index 8b6e6a30b2d..81351305ab7 100644 --- a/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/materials/physics_materials_cfg.py @@ -31,10 +31,6 @@ class RigidBodyMaterialCfg(PhysicsMaterialCfg): """Physics material parameters for rigid bodies. See :meth:`spawn_rigid_body_material` for more information. - - Note: - The default values are the `default values used by PhysX 5 - `__. """ func: Callable = physics_materials.spawn_rigid_body_material @@ -89,9 +85,6 @@ class DeformableBodyMaterialCfg(PhysicsMaterialCfg): See :meth:`spawn_deformable_body_material` for more information. - Note: - The default values are the `default values used by PhysX 5 - `__. """ func: Callable = physics_materials.spawn_deformable_body_material diff --git a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py index 2f90030ab3d..70206d4839f 100644 --- a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py +++ b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors_cfg.py @@ -24,10 +24,6 @@ class PinholeCameraCfg(SpawnerCfg): ..note :: Focal length as well as the aperture sizes and offsets are set as a tenth of the world unit. In our case, the world unit is Meter s.t. all of these values are set in cm. - - .. note:: - The default values are taken from the `Replicator camera `__ - function. """ func: Callable = sensors.spawn_camera diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index 9b75664c878..f4fa156704a 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -244,7 +244,7 @@ def _spawn_geom_from_prim_type( instancing and physics work. The rigid body component must be added to each instance and not the referenced asset (i.e. the prototype prim itself). This is because the rigid body component defines properties that are specific to each instance and cannot be shared under the referenced asset. For - more information, please check the `documentation `_. + more information, please check the `documentation `_. Due to the above, we follow the following structure: diff --git a/source/isaaclab/isaaclab/sim/utils.py b/source/isaaclab/isaaclab/sim/utils.py index 3d4b8fddb75..3cdbc182a6f 100644 --- a/source/isaaclab/isaaclab/sim/utils.py +++ b/source/isaaclab/isaaclab/sim/utils.py @@ -394,7 +394,7 @@ def bind_physics_material( The function is decorated with :meth:`apply_nested` to allow applying the function to a prim path and all its descendants. - .. _Physics material: https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/simulation-control/physics-settings.html#physics-materials + .. _Physics material: https://isaac-sim.github.io/IsaacLab/main/source/api/lab/isaaclab.sim.html#isaaclab.sim.SimulationCfg.physics_material Args: prim_path: The prim path where to apply the material. From 90dda53f7c08354b8c9ab3e509eae5b864a4040d Mon Sep 17 00:00:00 2001 From: ooctipus Date: Fri, 19 Sep 2025 15:11:15 -0700 Subject: [PATCH 47/50] Enhances Pbt usage experience through small improvements (#3449) # Description This PR is added with feedback from PBT user, and made below improvments 1. added resume logic to allow wandb to continue on the same run_id 2. corrected broadcasting order in distributed setup 3. made score query general by using dotted keys to access dictionary of arbitrary depth Fixes # (issue) - Bug fix (non-breaking change which fixes an issue) ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Co-authored-by: Kelly Guo --- .../features/population_based_training.rst | 8 ++++---- .../reinforcement_learning/rl_games/train.py | 5 +++-- source/isaaclab_rl/config/extension.toml | 2 +- source/isaaclab_rl/docs/CHANGELOG.rst | 12 ++++++++++++ .../isaaclab_rl/rl_games/pbt/pbt.py | 18 +++++++++++++----- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/source/features/population_based_training.rst b/docs/source/features/population_based_training.rst index b2a74d96457..49c4d370ff8 100644 --- a/docs/source/features/population_based_training.rst +++ b/docs/source/features/population_based_training.rst @@ -49,7 +49,7 @@ Example Config num_policies: 8 directory: . workspace: "pbt_workspace" - objective: Curriculum/difficulty_level + objective: episode.Curriculum/difficulty_level interval_steps: 50000000 threshold_std: 0.1 threshold_abs: 0.025 @@ -66,9 +66,9 @@ Example Config agent.params.config.tau: "mutate_discount" -``objective: Curriculum/difficulty_level`` uses ``infos["episode"]["Curriculum/difficulty_level"]`` as the scalar to -**rank policies** (higher is better). With ``num_policies: 8``, launch eight processes sharing the same ``workspace`` -and unique ``policy_idx`` (0-7). +``objective: episode.Curriculum/difficulty_level`` is the dotted expression that uses +``infos["episode"]["Curriculum/difficulty_level"]`` as the scalar to **rank policies** (higher is better). +With ``num_policies: 8``, launch eight processes sharing the same ``workspace`` and unique ``policy_idx`` (0-7). Launching PBT diff --git a/scripts/reinforcement_learning/rl_games/train.py b/scripts/reinforcement_learning/rl_games/train.py index 4d128ce420a..7f7346fc932 100644 --- a/scripts/reinforcement_learning/rl_games/train.py +++ b/scripts/reinforcement_learning/rl_games/train.py @@ -226,8 +226,9 @@ def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agen monitor_gym=True, save_code=True, ) - wandb.config.update({"env_cfg": env_cfg.to_dict()}) - wandb.config.update({"agent_cfg": agent_cfg}) + if not wandb.run.resumed: + wandb.config.update({"env_cfg": env_cfg.to_dict()}) + wandb.config.update({"agent_cfg": agent_cfg}) if args_cli.checkpoint is not None: runner.run({"train": True, "play": False, "sigma": train_sigma, "checkpoint": resume_path}) diff --git a/source/isaaclab_rl/config/extension.toml b/source/isaaclab_rl/config/extension.toml index 3539dcbacc2..8bf741bf288 100644 --- a/source/isaaclab_rl/config/extension.toml +++ b/source/isaaclab_rl/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.4.0" +version = "0.4.1" # Description title = "Isaac Lab RL" diff --git a/source/isaaclab_rl/docs/CHANGELOG.rst b/source/isaaclab_rl/docs/CHANGELOG.rst index 98e1115d9ba..25579ad2f45 100644 --- a/source/isaaclab_rl/docs/CHANGELOG.rst +++ b/source/isaaclab_rl/docs/CHANGELOG.rst @@ -1,6 +1,18 @@ Changelog --------- +0.4.1 (2025-09-09) +~~~~~~~~~~~~~~~~~~ + +Fixed +^^^^^ + +* Made PBT a bit nicer by +* 1. added resume logic to allow wandb to continue on the same run_id +* 2. corrected broadcasting order in distributed setup +* 3. made score query general by using dotted keys to access dictionary of arbitrary depth + + 0.4.0 (2025-09-09) ~~~~~~~~~~~~~~~~~~ diff --git a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py index dbff143d19c..714d5eea183 100644 --- a/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py +++ b/source/isaaclab_rl/isaaclab_rl/rl_games/pbt/pbt.py @@ -68,9 +68,12 @@ def process_infos(self, infos, done_indices): """Extract the scalar objective from environment infos and store in `self.score`. Notes: - Expects the objective to be at `infos["episode"][self.cfg.objective]`. + Expects the objective to be at `infos[self.cfg.objective]` where self.cfg.objective is dotted address. """ - self.score = infos["episode"][self.cfg.objective] + score = infos + for part in self.cfg.objective.split("."): + score = score[part] + self.score = score def after_steps(self): """Main PBT tick executed every train step. @@ -84,6 +87,9 @@ def after_steps(self): whitelisted params, set `restart_flag`, broadcast (if distributed), and print a mutation diff table. """ + if self.distributed_args.distributed: + dist.broadcast(self.restart_flag, src=0) + if self.distributed_args.rank != 0: if self.restart_flag.cpu().item() == 1: os._exit(0) @@ -154,9 +160,6 @@ def after_steps(self): self.new_params = mutate(cur_params, self.cfg.mutation, self.cfg.mutation_rate, self.cfg.change_range) self.restart_from_checkpoint = os.path.abspath(ckpts[replacement_policy_candidate]["checkpoint"]) self.restart_flag[0] = 1 - if self.distributed_args.distributed: - dist.broadcast(self.restart_flag, src=0) - self.printer.print_mutation_diff(cur_params, self.new_params) def _restart_with_new_params(self, new_params, restart_from_checkpoint): @@ -191,6 +194,11 @@ def _restart_with_new_params(self, new_params, restart_from_checkpoint): if self.wandb_args.enabled: import wandb + # note setdefault will only affect child process, that mean don't have to worry it env variable + # propagate beyond restarted child process + os.environ.setdefault("WANDB_RUN_ID", wandb.run.id) # continue with the same run id + os.environ.setdefault("WANDB_RESUME", "allow") # allow wandb to resume + os.environ.setdefault("WANDB_INIT_TIMEOUT", "300") # give wandb init more time to be fault tolerant wandb.run.finish() # Get the directory of the current file From 3a0db9d761982dc65417e6c6d0714cec61ceadb3 Mon Sep 17 00:00:00 2001 From: rebeccazhang0707 <168459200+rebeccazhang0707@users.noreply.github.com> Date: Tue, 23 Sep 2025 05:00:52 +0800 Subject: [PATCH 48/50] Updates default viewer pose to see the whole scene for Agibot environment (#3516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Fixes bug for toy object intermittently spawning behind the box for the task Isaac-Place-Toy2Box-Agibot-Right-Arm-RmpFlow-v0 - Reset the default viewer camera pose to see the whole scene. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Screenshots image ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../place/config/agibot/place_toy2box_rmp_rel_env_cfg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py index 0fa7fd9cafa..18d8ccdf1cb 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/place/config/agibot/place_toy2box_rmp_rel_env_cfg.py @@ -198,6 +198,10 @@ def __post_init__(self): self.sim.physx.gpu_total_aggregate_pairs_capacity = 16 * 1024 self.sim.physx.friction_correlation_distance = 0.00625 + # set viewer to see the whole scene + self.viewer.eye = [1.5, -1.0, 1.5] + self.viewer.lookat = [0.5, 0.0, 0.0] + """ Env to Replay Sim2Lab Demonstrations with JointSpaceAction From cd204adf3b6f663299830386071edcaafe9e0ed4 Mon Sep 17 00:00:00 2001 From: ooctipus Date: Tue, 23 Sep 2025 18:55:14 -0700 Subject: [PATCH 49/50] Adds torch hook to export libgomp.so.1 from installed torch (#3512) # Description This PR makes the LD_PRELOAD always points to the right python libgomp.so.1 when conda activate is invoked. The ARM machines doesn't really work with conda unless LD_PRELOAD is point to `torch/lib/libgomp.so.1` This has been tested to work on linux and arm to work on both machines ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Screenshots Please attach before and after screenshots of the change if applicable. ## Checklist - [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- isaaclab.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/isaaclab.sh b/isaaclab.sh index 5d5bc684641..263ff88e8a2 100755 --- a/isaaclab.sh +++ b/isaaclab.sh @@ -247,6 +247,45 @@ install_isaaclab_extension() { fi } +# Resolve Torch-bundled libgomp and prepend to LD_PRELOAD, once per shell session +write_torch_gomp_hooks() { + mkdir -p "${CONDA_PREFIX}/etc/conda/activate.d" "${CONDA_PREFIX}/etc/conda/deactivate.d" + + # activation: resolve Torch's libgomp via this env's Python and prepend to LD_PRELOAD + cat > "${CONDA_PREFIX}/etc/conda/activate.d/torch_gomp.sh" <<'EOS' +# Resolve Torch-bundled libgomp and prepend to LD_PRELOAD (quiet + idempotent) +: "${_IL_PREV_LD_PRELOAD:=${LD_PRELOAD-}}" + +__gomp="$("$CONDA_PREFIX/bin/python" - <<'PY' 2>/dev/null || true +import pathlib +try: + import torch + p = pathlib.Path(torch.__file__).parent / 'lib' / 'libgomp.so.1' + print(p if p.exists() else "", end="") +except Exception: + pass +PY +)" + +if [ -n "$__gomp" ] && [ -r "$__gomp" ]; then + case ":${LD_PRELOAD:-}:" in + *":$__gomp:"*) : ;; # already present + *) export LD_PRELOAD="$__gomp${LD_PRELOAD:+:$LD_PRELOAD}";; + esac +fi +unset __gomp +EOS + + # deactivation: restore original LD_PRELOAD + cat > "${CONDA_PREFIX}/etc/conda/deactivate.d/torch_gomp_unset.sh" <<'EOS' +# restore LD_PRELOAD to pre-activation value +if [ -v _IL_PREV_LD_PRELOAD ]; then + export LD_PRELOAD="$_IL_PREV_LD_PRELOAD" + unset _IL_PREV_LD_PRELOAD +fi +EOS +} + # setup anaconda environment for Isaac Lab setup_conda_env() { # get environment name from input @@ -311,6 +350,7 @@ setup_conda_env() { 'export RESOURCE_NAME="IsaacSim"' \ '' > ${CONDA_PREFIX}/etc/conda/activate.d/setenv.sh + write_torch_gomp_hooks # check if we have _isaac_sim directory -> if so that means binaries were installed. # we need to setup conda variables to load the binaries local isaacsim_setup_conda_env_script=${ISAACLAB_PATH}/_isaac_sim/setup_conda_env.sh From bd547aa60873ae8d696197ba9f233da36f65110d Mon Sep 17 00:00:00 2001 From: Cathy Li <40371641+cathyliyuanchen@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:39:27 -0700 Subject: [PATCH 50/50] Adds instructions on how to position the lighthouse for manus+vive (#3496) # Description Adds instructions on how to position the lighthouse for manus+vive, to resolve vive tracking quality issues that users may affect hand tracking experience. ## Type of change - Documentation update ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there Co-authored-by: Cathy Li --- docs/source/how-to/cloudxr_teleoperation.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/how-to/cloudxr_teleoperation.rst b/docs/source/how-to/cloudxr_teleoperation.rst index 3a03b283589..237227f4d64 100644 --- a/docs/source/how-to/cloudxr_teleoperation.rst +++ b/docs/source/how-to/cloudxr_teleoperation.rst @@ -409,7 +409,7 @@ Manus + Vive Hand Tracking Manus gloves and HTC Vive trackers can provide hand tracking when optical hand tracking from a headset is occluded. This setup expects Manus gloves with a Manus SDK license and Vive trackers attached to the gloves. -Requires Isaac Sim >=5.1. +Requires Isaac Sim 5.1 or later. Run the teleoperation example with Manus + Vive tracking: @@ -425,6 +425,10 @@ Begin the session with your palms facing up. This is necessary for calibrating Vive tracker poses using Apple Vision Pro wrist poses from a few initial frames, as the Vive trackers attached to the back of the hands occlude the optical hand tracking. +For optimal performance, position the lighthouse above the hands, tilted slightly downward. +One lighthouse is sufficient if both hands are visible. +Ensure the lighthouse remains stable; a stand is recommended to prevent wobbling. + .. note:: To avoid resource contention and crashes, ensure Manus and Vive devices are connected to different USB controllers/buses.